From ba3a68cf8a7c3305f4eb35ab708f8bd637f5c156 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Mon, 27 Jun 2022 10:11:26 +0700 Subject: [PATCH 1/9] Restructure Artifact Service --- .../artifact/api/ArtifactCoordinates.java | 67 ++-- .../downloads/artifact/api/Group.java | 62 +-- .../artifact/api/MavenCoordinates.java | 2 +- .../downloads/artifact/api/Ordering.java | 42 -- .../downloads/artifact/api/VersionType.java | 12 +- .../artifact/api/event/GroupUpdate.java | 2 +- .../artifact/api/query/ArtifactDetails.java | 12 +- .../api/query/ArtifactRegistration.java | 137 +------ .../api/query/GetArtifactsResponse.java | 72 +--- .../artifact/api/query/GroupRegistration.java | 70 +--- .../artifact/api/query/GroupResponse.java | 60 +-- .../artifact/api/query/GroupsResponse.java | 32 +- .../downloads/artifact/ArtifactModule.java | 5 +- .../artifact/ArtifactServiceImpl.java | 361 ------------------ .../details/ArtifactDetailsEntity.java | 8 +- .../artifact/details/DetailsManager.java | 132 +++++++ .../details/state/PopulatedState.java | 2 +- .../errors/GitRemoteValidationException.java | 40 -- .../artifact/global/GlobalManager.java | 51 +++ .../artifact/group/GroupCommand.java | 156 +------- .../downloads/artifact/group/GroupEntity.java | 52 +-- .../downloads/artifact/group/GroupEvent.java | 57 +-- .../artifact/group/GroupManager.java | 96 +++++ .../artifact/group/state/EmptyState.java | 10 +- .../artifact/group/state/GroupState.java | 10 +- .../artifact/group/state/PopulatedState.java | 39 +- .../artifact/readside/ArtifactReadside.java | 8 +- .../transport/RestArtifactService.java | 212 ++++++++++ .../test/akka/EventBehaviorTestkit.java | 27 ++ .../test/global/GlobalArtifactsTest.java | 20 +- .../test/groups/GroupEntityCommandsTest.java | 126 ++++++ .../query/api/GetArtifactDetailsResponse.java | 42 +- .../gitmanaged/ScheduledCommitResolver.java | 2 +- .../util/jgit/RepositoryCloner.java | 2 +- .../synchronizer/resync/ResyncManager.java | 4 +- .../domain/ArtifactSynchronizerAggregate.java | 8 +- .../readside/VersionReadSidePersistence.java | 20 +- .../server/readside/VersionedTagWorker.java | 4 +- .../query/impl/VersionQueryServiceImpl.java | 12 +- 39 files changed, 861 insertions(+), 1215 deletions(-) delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java delete mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactServiceImpl.java create mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/DetailsManager.java delete mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java create mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java create mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java create mode 100644 artifact-impl/src/main/java/org/spongepowered/downloads/artifact/transport/RestArtifactService.java create mode 100644 artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/akka/EventBehaviorTestkit.java create mode 100644 artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/groups/GroupEntityCommandsTest.java diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java index a09f77b3..99131c11 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java @@ -24,64 +24,43 @@ */ package org.spongepowered.downloads.artifact.api; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import java.util.Objects; import java.util.StringJoiner; @JsonDeserialize -public final class ArtifactCoordinates { +public record ArtifactCoordinates( + @JsonProperty(required = true) String groupId, + @JsonProperty(required = true) String artifactId +) { + @JsonCreator + public ArtifactCoordinates { + } + + public MavenCoordinates version(String version) { + return MavenCoordinates.parse( + new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); + } + + public String asMavenString() { + return this.groupId() + ":" + this.artifactId(); + } /** * The group id of an artifact, as defined by the Apache Maven documentation. * See Maven Coordinates. */ - @JsonProperty(required = true) - public final String groupId; + public String groupId() { + return groupId; + } + /** * The artifact id of an artifact, as defined by the Apache Maven documentation. * See Maven Coordinates. */ - @JsonProperty(required = true) - public final String artifactId; - - - public ArtifactCoordinates(final String groupId, final String artifactId) { - this.groupId = groupId; - this.artifactId = artifactId; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ArtifactCoordinates that = (ArtifactCoordinates) o; - return Objects.equals(groupId, that.groupId) && Objects.equals(artifactId, that.artifactId); - } - - @Override - public int hashCode() { - return Objects.hash(groupId, artifactId); - } - - @Override - public String toString() { - return new StringJoiner(", ", ArtifactCoordinates.class.getSimpleName() + "[", "]") - .add("groupId='" + groupId + "'") - .add("artifactId='" + artifactId + "'") - .toString(); - } - - public MavenCoordinates version(String version) { - return MavenCoordinates.parse(new StringJoiner(":").add(this.groupId).add(this.artifactId).add(version).toString()); - } - - public String asMavenString() { - return this.groupId + ":" + this.artifactId; + public String artifactId() { + return artifactId; } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java index 9a888203..7e9145d1 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java @@ -28,65 +28,15 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import java.util.Objects; -import java.util.StringJoiner; - @JsonDeserialize -public final class Group { - - @JsonProperty(required = true) - public final String groupCoordinates; - @JsonProperty(required = true) - public final String name; - @JsonProperty(required = true) - public final String website; +public record Group( + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String website +) { @JsonCreator - public Group(final String groupCoordinates, final String name, final String website) { - this.groupCoordinates = groupCoordinates; - this.name = name; - this.website = website; - } - - public String getGroupCoordinates() { - return this.groupCoordinates; + public Group { } - public String getName() { - return this.name; - } - - public String getWebsite() { - return this.website; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final Group group = (Group) o; - return Objects.equals(this.groupCoordinates, group.groupCoordinates) && - Objects.equals(this.name, group.name) && - Objects.equals(this.website, group.website); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupCoordinates, this.name, this.website); - } - - @Override - public String - toString() { - return new StringJoiner( - ", ", Group.class.getSimpleName() + "[", "]") - .add("groupCoordinates='" + this.groupCoordinates + "'") - .add("name='" + this.name + "'") - .add("website=" + this.website) - .toString(); - } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java index 5735f1de..c1acb521 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java @@ -37,7 +37,7 @@ @JsonDeserialize public final class MavenCoordinates implements Comparable { - private static final Pattern MAVEN_REGEX = Pattern.compile("[-\\w.]+"); + private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); /** * The group id of an artifact, as defined by the Apache Maven documentation. diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java deleted file mode 100644 index 03612b57..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java +++ /dev/null @@ -1,42 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public enum Ordering { - Ascending("asc"), - Descending("desc"); - - @JsonValue - public final String representation; - - - Ordering(final String representation) { - this.representation = representation; - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java index 3c89badf..471c4a5c 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java @@ -52,8 +52,7 @@ public String asStandardVersionString(final String version) { stringJoiner.add(split[i]); } - final var unTimestampedVersion = stringJoiner.add(SNAPSHOT_VERSION).toString(); - return unTimestampedVersion; + return stringJoiner.add(SNAPSHOT_VERSION).toString(); } }, @@ -82,9 +81,9 @@ public boolean isSnapshot() { Verifies the pattern that the snapshot version is date.time-build formatted, enables the pattern match for a timestamped snapshot */ - private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-([0-9]{8}.[0-9]{6})-([0-9]+)$"); + private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-(\\d{8}.\\d{6})-(\\d+)$"); - private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("([0-9]{8}.[0-9]{6})-([0-9]+)$"); + private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("(\\d{8}.\\d{6})-(\\d+)$"); public static VersionType fromVersion(final String version) { if (version == null || version.isEmpty()) { @@ -106,11 +105,6 @@ public static VersionType fromVersion(final String version) { return RELEASE; } - /** - * Gets whether this version is a snapshot of any kind. - * - * @return - */ public boolean isSnapshot() { return false; } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java index 21518df1..970a873e 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java @@ -67,7 +67,7 @@ final record ArtifactRegistered(ArtifactCoordinates coordinates) implements Grou @Override public String groupId() { - return this.coordinates.groupId; + return this.coordinates.groupId(); } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java index a1f89443..53c9567f 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java @@ -51,11 +51,11 @@ public final class ArtifactDetails { name = "gitRepository"), }) @JsonDeserialize - public interface Update { + public sealed interface Update { Either validate(); - final record Website( + record Website( @JsonProperty(required = true) String website ) implements Update { @@ -70,7 +70,7 @@ public Either validate() { } } - final record DisplayName( + record DisplayName( @JsonProperty(required = true) String display ) implements Update { @@ -84,7 +84,7 @@ public Either validate() { } } - final record Issues( + record Issues( @JsonProperty(required = true) String issues ) implements Update { @JsonCreator @@ -98,7 +98,7 @@ public Either validate() { } } - final record GitRepository( + record GitRepository( @JsonProperty(required = true) String gitRepo ) implements Update { @@ -115,7 +115,7 @@ public Either validate() { } @JsonSerialize - public final record Response( + public record Response( String name, String displayName, String website, diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java index 3da5777a..362cdd88 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java @@ -32,19 +32,13 @@ import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import java.io.Serial; -import java.util.Objects; -import java.util.StringJoiner; - public final class ArtifactRegistration { @JsonSerialize - public static final class RegisterArtifact { - - @JsonProperty(required = true) - public final String artifactId; - @JsonProperty(required = true) - public final String displayName; + public record RegisterArtifact( + @JsonProperty(required = true) String artifactId, + @JsonProperty(required = true) String displayName + ) { @JsonCreator public RegisterArtifact(final String artifactId, final String displayName) { @@ -52,129 +46,36 @@ public RegisterArtifact(final String artifactId, final String displayName) { this.displayName = displayName; } - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final RegisterArtifact that = (RegisterArtifact) o; - return Objects.equals(this.artifactId, that.artifactId) - && Objects.equals(this.displayName, that.displayName); - } - - @Override - public int hashCode() { - return Objects.hash(this.artifactId, this.displayName); - } - - @Override - public String toString() { - return new StringJoiner( - ", ", - RegisterArtifact.class.getSimpleName() + "[", - "]" - ) - .add("artifactId='" + this.artifactId + "'") - .add("displayName='" + this.displayName + "'") - .toString(); - } } - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, name = "UnknownGroup"), - @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, name = "RegisteredArtifact"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, name = "AlreadyRegistered"), + @JsonSubTypes.Type(value = Response.GroupMissing.class, + name = "UnknownGroup"), + @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, + name = "RegisteredArtifact"), + @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, + name = "AlreadyRegistered"), }) - public interface Response extends Jsonable { + public sealed interface Response extends Jsonable { @JsonSerialize - final class ArtifactRegistered implements Response { - - - @Serial private static final long serialVersionUID = 8946348744839402438L; - - @JsonProperty public final ArtifactCoordinates coordinates; - - public ArtifactRegistered(ArtifactCoordinates coordinates) { - this.coordinates = coordinates; - } + record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { } @JsonSerialize - final class ArtifactAlreadyRegistered implements Response { + record ArtifactAlreadyRegistered( + @JsonProperty String artifactName, + @JsonProperty String groupId + ) implements Response { - @Serial private static final long serialVersionUID = -3135793273231868113L; - - @JsonProperty - public final String artifactName; - @JsonProperty - public final String groupId; - - public ArtifactAlreadyRegistered(final String artifactName, final String groupId) { - this.artifactName = artifactName; - this.groupId = groupId; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (ArtifactAlreadyRegistered) obj; - return Objects.equals(this.artifactName, that.artifactName) && - Objects.equals(this.groupId, that.groupId); - } - - @Override - public int hashCode() { - return Objects.hash(this.artifactName, this.groupId); - } - - @Override - public String toString() { - return "ArtifactAlreadyRegistered[" + - "artifactName=" + this.artifactName + ", " + - "groupId=" + this.groupId + ']'; - } } @JsonSerialize - final class GroupMissing implements Response { - @Serial private static final long serialVersionUID = 8763121568817311891L; - - @JsonProperty("groupId") - private final String s; - - public GroupMissing(final String s) { - this.s = s; - } - - public String s() { - return this.s; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (GroupMissing) obj; - return Objects.equals(this.s, that.s); - } - - @Override - public int hashCode() { - return Objects.hash(this.s); - } + record GroupMissing(@JsonProperty("groupId") String s) implements Response { - @Override - public String toString() { - return "GroupMissing[" + - "s=" + this.s + ']'; - } } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java index 3a08df91..eec64a44 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java @@ -29,89 +29,31 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.lightbend.lagom.serialization.Jsonable; import io.vavr.collection.List; -import java.util.Objects; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = GetArtifactsResponse.GroupMissing.class, name = "UnknownGroup"), @JsonSubTypes.Type(value = GetArtifactsResponse.ArtifactsAvailable.class, name = "Artifacts"), }) -public interface GetArtifactsResponse { +public sealed interface GetArtifactsResponse extends Jsonable { @JsonSerialize - final class GroupMissing implements GetArtifactsResponse { - - @JsonProperty - private final String groupRequested; + record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { @JsonCreator - public GroupMissing( - final String groupRequested - ) { - this.groupRequested = groupRequested; - } - - public String groupRequested() { - return this.groupRequested; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (GroupMissing) obj; - return Objects.equals(this.groupRequested, that.groupRequested); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupRequested); - } - - @Override - public String toString() { - return "GroupMissing[" + - "groupRequested=" + this.groupRequested + ']'; + public GroupMissing { } } @JsonSerialize - final class ArtifactsAvailable implements GetArtifactsResponse { - - @JsonProperty - private final List artifactIds; + record ArtifactsAvailable(@JsonProperty List artifactIds) + implements GetArtifactsResponse { @JsonCreator - public ArtifactsAvailable( - final List artifactIds - ) { - this.artifactIds = artifactIds; - } - - public List artifactIds() { - return this.artifactIds; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (ArtifactsAvailable) obj; - return Objects.equals(this.artifactIds, that.artifactIds); - } - - @Override - public int hashCode() { - return Objects.hash(this.artifactIds); - } - - @Override - public String toString() { - return "ArtifactsAvailable[" + - "artifactIds=" + this.artifactIds + ']'; + public ArtifactsAvailable { } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java index aa580ade..81e534ae 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java @@ -30,7 +30,6 @@ import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.Group; -import java.io.Serial; import java.util.Objects; public final class GroupRegistration { @@ -67,8 +66,12 @@ public RegisterGroupRequest( @Override public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } final var that = (RegisterGroupRequest) obj; return Objects.equals(this.name, that.name) && Objects.equals(this.groupCoordinates, that.groupCoordinates) && @@ -91,68 +94,11 @@ public String toString() { public interface Response extends Jsonable { - final class GroupAlreadyRegistered implements Response { - @Serial private static final long serialVersionUID = 0L; - private final String groupNameRequested; - - public GroupAlreadyRegistered(final String groupNameRequested) { - this.groupNameRequested = groupNameRequested; - } - - public String groupNameRequested() { - return this.groupNameRequested; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (GroupAlreadyRegistered) obj; - return Objects.equals(this.groupNameRequested, that.groupNameRequested); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupNameRequested); - } - - @Override - public String toString() { - return "GroupAlreadyRegistered[" + - "groupNameRequested=" + this.groupNameRequested + ']'; - } + record GroupAlreadyRegistered(String groupNameRequested) implements Response { } - final class GroupRegistered implements Response { - @Serial private static final long serialVersionUID = 0L; - private final Group group; - - public GroupRegistered(final Group group) { - this.group = group; - } - - public Group group() { - return this.group; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (GroupRegistered) obj; - return Objects.equals(this.group, that.group); - } + record GroupRegistered(Group group) implements Response { - @Override - public int hashCode() { - return Objects.hash(this.group); - } - - @Override - public String toString() { - return "GroupRegistered[" + - "group=" + this.group + ']'; - } } } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java index f17c7d87..a973e23f 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java @@ -29,83 +29,33 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.Group; -import java.util.Objects; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") }) -public interface GroupResponse { +public sealed interface GroupResponse extends Jsonable { @JsonSerialize - final class Missing implements GroupResponse { - @JsonProperty - public final String groupId; - + record Missing(@JsonProperty String groupId) implements GroupResponse { @JsonCreator public Missing(final String groupId) { this.groupId = groupId; } - public String groupId() { - return this.groupId; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (Missing) obj; - return Objects.equals(this.groupId, that.groupId); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId); - } - - @Override - public String toString() { - return "Missing[" + - "groupId=" + this.groupId + ']'; - } } - @JsonSerialize - final class Available implements GroupResponse { - @JsonProperty - public final Group group; + @JsonSerialize + record Available(@JsonProperty Group group) implements GroupResponse { @JsonCreator public Available(final Group group) { this.group = group; } - public Group group() { - return this.group; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (Available) obj; - return Objects.equals(this.group, that.group); - } - - @Override - public int hashCode() { - return Objects.hash(this.group); - } - - @Override - public String toString() { - return "Available[" + - "group=" + this.group + ']'; - } } } diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java index 4d1940d3..1332d045 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java @@ -32,8 +32,6 @@ import io.vavr.collection.List; import org.spongepowered.downloads.artifact.api.Group; -import java.util.Objects; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") @@ -41,34 +39,10 @@ public interface GroupsResponse { @JsonSerialize - final class Available implements GroupsResponse { - - @JsonProperty - public final List groups; - + record Available(@JsonProperty List groups) + implements GroupsResponse { @JsonCreator - public Available(final List groups) { - this.groups = groups; - } - - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (Available) obj; - return Objects.equals(this.groups, that.groups); - } - - @Override - public int hashCode() { - return Objects.hash(this.groups); - } - - @Override - public String toString() { - return "Available[" + - "group=" + this.groups + ']'; + public Available { } } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactModule.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactModule.java index de787e7d..ecf44857 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactModule.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactModule.java @@ -30,12 +30,12 @@ import org.pac4j.core.config.Config; import org.spongepowered.downloads.artifact.api.ArtifactService; import org.spongepowered.downloads.artifact.readside.ArtifactReadside; +import org.spongepowered.downloads.artifact.transport.RestArtifactService; import org.spongepowered.downloads.auth.SOADAuth; import org.spongepowered.downloads.auth.api.utils.AuthUtils; import play.Environment; public class ArtifactModule extends AbstractModule implements ServiceGuiceSupport { - private final Environment environment; private final com.typesafe.config.Config config; private final AuthUtils auth; @@ -46,10 +46,9 @@ public ArtifactModule(final Environment environment, final com.typesafe.config.C this.auth = AuthUtils.configure(config); } - @Override protected void configure() { - this.bindService(ArtifactService.class, ArtifactServiceImpl.class); + this.bindService(ArtifactService.class, RestArtifactService.class); this.bind(ArtifactReadside.class).asEagerSingleton(); } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactServiceImpl.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactServiceImpl.java deleted file mode 100644 index 4faffc3b..00000000 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/ArtifactServiceImpl.java +++ /dev/null @@ -1,361 +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.artifact; - -import akka.Done; -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.japi.Pair; -import akka.persistence.typed.PersistenceId; -import com.google.inject.Inject; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.broker.Topic; -import com.lightbend.lagom.javadsl.api.transport.BadRequest; -import com.lightbend.lagom.javadsl.api.transport.NotFound; -import com.lightbend.lagom.javadsl.broker.TopicProducer; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.Offset; -import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; -import com.lightbend.lagom.javadsl.server.ServerServiceCall; -import io.vavr.API; -import io.vavr.Predicates; -import io.vavr.control.Either; -import io.vavr.control.Try; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.InvalidRemoteException; -import org.pac4j.core.config.Config; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; -import org.spongepowered.downloads.artifact.api.event.GroupUpdate; -import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -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.query.GroupsResponse; -import org.spongepowered.downloads.artifact.details.ArtifactDetailsEntity; -import org.spongepowered.downloads.artifact.details.DetailsCommand; -import org.spongepowered.downloads.artifact.details.DetailsEvent; -import org.spongepowered.downloads.artifact.errors.GitRemoteValidationException; -import org.spongepowered.downloads.artifact.global.GlobalCommand; -import org.spongepowered.downloads.artifact.global.GlobalRegistration; -import org.spongepowered.downloads.artifact.group.GroupCommand; -import org.spongepowered.downloads.artifact.group.GroupEntity; -import org.spongepowered.downloads.artifact.group.GroupEvent; -import org.spongepowered.downloads.auth.AuthenticatedInternalService; -import org.spongepowered.downloads.auth.SOADAuth; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; - -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; - -public class ArtifactServiceImpl implements ArtifactService, - AuthenticatedInternalService { - - private final Duration askTimeout = Duration.ofHours(5); - private final Config securityConfig; - private final ClusterSharding clusterSharding; - private final PersistentEntityRegistry persistentEntityRegistry; - private final AuthUtils auth; - - @Inject - public ArtifactServiceImpl( - final ClusterSharding clusterSharding, - final PersistentEntityRegistry persistentEntityRegistry, - final AuthUtils auth, - @SOADAuth final Config securityConfig - ) { - this.clusterSharding = clusterSharding; - this.securityConfig = securityConfig; - this.auth = auth; - this.clusterSharding.init( - Entity.of( - GroupEntity.ENTITY_TYPE_KEY, - GroupEntity::create - ) - ); - this.clusterSharding.init( - Entity.of( - GlobalRegistration.ENTITY_TYPE_KEY, - ctx -> GlobalRegistration.create( - ctx.getEntityId(), - PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()) - ) - ) - ); - this.clusterSharding.init( - Entity.of( - ArtifactDetailsEntity.ENTITY_TYPE_KEY, - context -> ArtifactDetailsEntity.create( - context, - context.getEntityId(), - PersistenceId.of(context.getEntityTypeKey().name(), context.getEntityId()) - ) - ) - ); - this.persistentEntityRegistry = persistentEntityRegistry; - } - - @Override - public ServiceCall getArtifacts(final String groupId) { - return none -> this.getGroupEntity(groupId) - .ask(replyTo -> new GroupCommand.GetArtifacts(groupId, replyTo), this.askTimeout); - } - - @Override - public ServiceCall registerGroup() { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> registration -> { - final String mavenCoordinates = registration.groupCoordinates; - final String name = registration.name; - final String website = registration.website; - return this.getGroupEntity(registration.groupCoordinates.toLowerCase(Locale.ROOT)) - .ask( - replyTo -> new GroupCommand.RegisterGroup(mavenCoordinates, name, website, replyTo), - this.askTimeout - ).thenCompose(response -> { - if (!(response instanceof GroupRegistration.Response.GroupRegistered)) { - return CompletableFuture.completedFuture(response); - } - final var group = ((GroupRegistration.Response.GroupRegistered) response).group(); - return this.getGlobalEntity() - .ask(replyTo -> new GlobalCommand.RegisterGroup(replyTo, group), this.askTimeout) - .thenApply(notUsed -> response); - - }); - }); - } - - @Override - public ServiceCall, ArtifactDetails.Response> updateDetails( - final String groupId, - final String artifactId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> update -> { - final var coords = ArtifactServiceImpl.validateCoordinates(groupId, artifactId).get(); - - // Java 17 preview feature allows for switch matching here - // Waiting on SOAD-16 - if (update instanceof ArtifactDetails.Update.Website w) { - final var validate = w.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var validUrl = validate.get(); - final var response = this.getDetailsEntity(groupId, artifactId) - .>ask( - r -> new DetailsCommand.UpdateWebsite(coords, validUrl, r), this.askTimeout); - return response.thenApply(Either::get); - } else if (update instanceof ArtifactDetails.Update.DisplayName d) { - final var validate = d.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var displayName = validate.get(); - final var response = this.getDetailsEntity(groupId, artifactId) - .>ask( - r -> new DetailsCommand.UpdateDisplayName(coords, displayName, r), - this.askTimeout - ); - return response.thenApply(Either::get); - } else if (update instanceof ArtifactDetails.Update.GitRepository gr) { - final var validate = gr.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var invalidRemote = API.Case( - API.$(Predicates.instanceOf(InvalidRemoteException.class)), - () -> new BadRequest(String.format("Invalid remote: %s", gr.gitRepo())) - ); - final var genericRemoteProblem = API.Case( - API.$(Predicates.instanceOf(GitAPIException.class)), - t -> new GitRemoteValidationException("Error resolving repository", t) - ); - - @SuppressWarnings("unchecked") final var of = Try.of( - () -> Git.lsRemoteRepository() - .setRemote(gr.gitRepo()) - .call() - ).mapFailure(invalidRemote, genericRemoteProblem) - .map(refs -> !refs.isEmpty()) - .flatMap(remoteValid -> { - if (!remoteValid) { - return Try.failure(new GitRemoteValidationException("Remote repository has no refs")); - } - return Try.success(gr.gitRepo()); - }) - .get(); - - final var response = this.getDetailsEntity(groupId, artifactId) - .>ask( - r -> new DetailsCommand.UpdateGitRepository(coords, of, r), this.askTimeout); - return response.thenApply(Either::get); - } else if (update instanceof ArtifactDetails.Update.Issues i) { - final var validate = i.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var validUrl = validate.get(); - final var response = this.getDetailsEntity(groupId, artifactId) - .>ask( - r -> new DetailsCommand.UpdateIssues(coords, validUrl, r), this.askTimeout); - return response.thenApply(Either::get); - } else { - throw new BadRequest(String.format("Unknown update type: %s", update)); - } - }); - } - - @Override - public ServerServiceCall registerArtifacts( - final String groupId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> registration -> { - final EntityRef groupEntity = this.getGroupEntity(groupId.toLowerCase(Locale.ROOT)); - final var artifactId = registration.artifactId; - return groupEntity - .ask( - replyTo -> new GroupCommand.RegisterArtifact(artifactId, replyTo), this.askTimeout) - .thenCompose(response -> { - if (!(response instanceof ArtifactRegistration.Response.ArtifactRegistered)) { - return CompletableFuture.completedFuture(response); - } - final var coordinates = ((ArtifactRegistration.Response.ArtifactRegistered) response).coordinates; - return this.getDetailsEntity( - coordinates.groupId, coordinates.artifactId) - .ask( - replyTo -> new DetailsCommand.RegisterArtifact( - coordinates, registration.displayName, replyTo), this.askTimeout) - .thenApply(notUsed -> response); - }); - }); - } - - @Override - public ServiceCall getGroup(final String groupId) { - return notUsed -> this.getGroupEntity(groupId.toLowerCase(Locale.ROOT)) - .ask(replyTo -> new GroupCommand.GetGroup(groupId, replyTo), this.askTimeout); - } - - @Override - public ServiceCall getGroups() { - return notUsed -> this.getGlobalEntity().ask(GlobalCommand.GetGroups::new, this.askTimeout); - } - - @Override - public Topic groupTopic() { - return TopicProducer.taggedStreamWithOffset( - GroupEvent.TAG.allTags(), - (AggregateEventTag aggregateTag, Offset fromOffset) -> - this.persistentEntityRegistry.eventStream(aggregateTag, fromOffset) - .mapConcat(ArtifactServiceImpl::convertEvent) - ); - } - - @Override - public Topic artifactUpdate() { - return TopicProducer.taggedStreamWithOffset( - DetailsEvent.TAG.allTags(), - (AggregateEventTag tag, Offset from) -> - this.persistentEntityRegistry.eventStream(tag, from) - .mapConcat(ArtifactServiceImpl::convertDetailsEvent) - ); - } - - private static List> convertEvent(Pair pair) { - if (pair.first() instanceof GroupEvent.ArtifactRegistered a) { - return Collections.singletonList( - Pair.create( - new GroupUpdate.ArtifactRegistered(new ArtifactCoordinates(a.groupId, a.artifact)), - pair.second() - )); - } else if (pair.first() instanceof GroupEvent.GroupRegistered g) { - return Collections.singletonList( - Pair.create(new GroupUpdate.GroupRegistered(g.groupId, g.name, g.website), pair.second())); - } - return Collections.emptyList(); - } - - private static List> convertDetailsEvent(Pair pair) { - final ArtifactUpdate message; - // Java 17+ Switch pattern matching will benefit here - // Or can use Vavr Match, but that seems overkill. - if (pair.first() instanceof DetailsEvent.ArtifactGitRepositoryUpdated repoUpdated) { - message = new ArtifactUpdate.GitRepositoryAssociated(repoUpdated.coordinates(), repoUpdated.gitRepo()); - } else if (pair.first() instanceof DetailsEvent.ArtifactRegistered registered) { - message = new ArtifactUpdate.ArtifactRegistered(registered.coordinates()); - } else if (pair.first() instanceof DetailsEvent.ArtifactDetailsUpdated details) { - message = new ArtifactUpdate.DisplayNameUpdated(details.coordinates(), details.displayName()); - } else if (pair.first() instanceof DetailsEvent.ArtifactIssuesUpdated details) { - message = new ArtifactUpdate.IssuesUpdated(details.coordinates(), details.url()); - } else if (pair.first() instanceof DetailsEvent.ArtifactWebsiteUpdated website) { - message = new ArtifactUpdate.WebsiteUpdated(website.coordinates(), website.url()); - } else { - return Collections.emptyList(); - } - return Collections.singletonList(Pair.create(message, pair.second())); - } - - private EntityRef getGroupEntity(final String groupId) { - return this.clusterSharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, groupId.toLowerCase(Locale.ROOT)); - } - - private EntityRef getDetailsEntity(final String groupId, final String artifactId) { - return this.clusterSharding.entityRefFor(ArtifactDetailsEntity.ENTITY_TYPE_KEY, groupId + ":" + artifactId); - } - - private EntityRef getGlobalEntity() { - return this.clusterSharding.entityRefFor(GlobalRegistration.ENTITY_TYPE_KEY, "global"); - } - - @Override - public Config getSecurityConfig() { - return this.securityConfig; - } - - @Override - public AuthUtils auth() { - return this.auth; - } - - private static Try validateCoordinates(final String groupId, final String artifactId) { - final var validGroupId = groupId.toLowerCase(Locale.ROOT).trim(); - final var validArtifactId = artifactId.toLowerCase(Locale.ROOT).trim(); - if (validGroupId.isEmpty()) { - return Try.failure(new NotFound("group not found")); - } - if (validArtifactId.isEmpty()) { - return Try.failure(new NotFound("artifact not found")); - } - final var coords = new ArtifactCoordinates(validGroupId, validArtifactId); - return Try.success(coords); - } -} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/ArtifactDetailsEntity.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/ArtifactDetailsEntity.java index 0858b29d..b4d51ca3 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/ArtifactDetailsEntity.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/ArtifactDetailsEntity.java @@ -142,7 +142,7 @@ public CommandHandlerWithReply comma cmd.replyTo(), us -> Either.right( new ArtifactDetails.Response( - us.coordinates().artifactId, + us.coordinates().artifactId(), us.displayName(), us.website(), us.issues(), @@ -159,7 +159,7 @@ public CommandHandlerWithReply comma cmd.replyTo(), us -> Either.right( new ArtifactDetails.Response( - us.coordinates().artifactId, + us.coordinates().artifactId(), us.displayName(), us.website(), us.issues(), @@ -176,7 +176,7 @@ public CommandHandlerWithReply comma cmd.replyTo(), us -> Either.right( new ArtifactDetails.Response( - us.coordinates().artifactId, + us.coordinates().artifactId(), us.displayName(), us.website(), us.issues(), @@ -193,7 +193,7 @@ public CommandHandlerWithReply comma cmd.replyTo(), us -> Either.right( new ArtifactDetails.Response( - us.coordinates().artifactId, + us.coordinates().artifactId(), us.displayName(), us.website(), us.issues(), diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/DetailsManager.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/DetailsManager.java new file mode 100644 index 00000000..369c16c5 --- /dev/null +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/DetailsManager.java @@ -0,0 +1,132 @@ +package org.spongepowered.downloads.artifact.details; + +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 com.lightbend.lagom.javadsl.api.transport.BadRequest; +import com.lightbend.lagom.javadsl.api.transport.NotFound; +import io.vavr.control.Either; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.lib.Ref; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; + +import java.net.URL; +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletionStage; + +public class DetailsManager { + private final ClusterSharding clusterSharding; + private final Duration askTimeout = Duration.ofHours(5); + + public DetailsManager(ClusterSharding clusterSharding) { + this.clusterSharding = clusterSharding; + this.clusterSharding.init( + Entity.of( + ArtifactDetailsEntity.ENTITY_TYPE_KEY, + context -> ArtifactDetailsEntity.create( + context, + context.getEntityId(), + PersistenceId.of(context.getEntityTypeKey().name(), context.getEntityId()) + ) + ) + ); + } + + public CompletionStage UpdateWebsite( + ArtifactCoordinates coords, ArtifactDetails.Update.Website w + ) { + final Either validate = w.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var validUrl = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateWebsite(coords, validUrl, r), this.askTimeout) + .thenApply(Either::get); + } + + + public CompletionStage UpdateDisplayName( + ArtifactCoordinates coords, ArtifactDetails.Update.DisplayName d + ) { + final Either validate = d.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var displayName = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateDisplayName(coords, displayName, r), + this.askTimeout + ) + .thenApply(Either::get); + } + + public CompletionStage UpdateGitRepository( + final ArtifactCoordinates coords, ArtifactDetails.Update.GitRepository gr + ) { + final Either validate = gr.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final Collection refs; + try { + refs = Git.lsRemoteRepository() + .setRemote(gr.gitRepo()) + .call(); + } catch (InvalidRemoteException e) { + throw new BadRequest(String.format("Invalid remote: %s", gr.gitRepo())); + } catch (GitAPIException e) { + throw new BadRequest(String.format("Error resolving repository '%s'", gr.gitRepo())); + } + if (refs.isEmpty()) { + throw new BadRequest(String.format("Remote repository '%s' has no refs", gr.gitRepo())); + } + + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateGitRepository(coords, gr.gitRepo(), r), this.askTimeout) + .thenApply(Either::get); + } + + public CompletionStage UpdateIssues( + ArtifactCoordinates coords, ArtifactDetails.Update.Issues i + ) { + final Either validate = i.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var validUrl = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateIssues(coords, validUrl, r), this.askTimeout).thenApply( + Either::get); + } + + private EntityRef getDetailsEntity(final ArtifactCoordinates coordinates) { + return this.getDetailsEntity(coordinates.groupId(), coordinates.artifactId()); + } + + private EntityRef getDetailsEntity(final String groupId, final String artifactId) { + return this.clusterSharding.entityRefFor(ArtifactDetailsEntity.ENTITY_TYPE_KEY, groupId + ":" + artifactId); + } + + public CompletionStage registerArtifact( + ArtifactRegistration.Response.ArtifactRegistered registered, + String displayName + ) { + return this.getDetailsEntity(registered.coordinates()) + .ask( + replyTo -> new DetailsCommand.RegisterArtifact( + registered.coordinates(), displayName, replyTo), this.askTimeout) + .thenApply(notUsed -> registered); + } +} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/state/PopulatedState.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/state/PopulatedState.java index 9cddaa8d..b0fb05ee 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/state/PopulatedState.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/details/state/PopulatedState.java @@ -40,7 +40,7 @@ public record PopulatedState(ArtifactCoordinates coordinates, } public boolean isEmpty() { - return this.coordinates.artifactId.isBlank() && this.coordinates.groupId.isBlank(); + return this.coordinates.artifactId().isBlank() && this.coordinates.groupId().isBlank(); } public DetailsState withDisplayName(DetailsEvent.ArtifactDetailsUpdated event) { diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java deleted file mode 100644 index 8a71aa45..00000000 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.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.artifact.errors; - -import com.lightbend.lagom.javadsl.api.transport.TransportErrorCode; -import com.lightbend.lagom.javadsl.api.transport.TransportException; - -public class GitRemoteValidationException extends TransportException { - - public GitRemoteValidationException(String message) { - super(TransportErrorCode.BadRequest, message); - } - - public GitRemoteValidationException(String message, final Throwable cause) { - super(TransportErrorCode.BadRequest, message, cause); - } - -} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java new file mode 100644 index 00000000..804c7c39 --- /dev/null +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java @@ -0,0 +1,51 @@ +package org.spongepowered.downloads.artifact.global; + +import akka.Done; +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 org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; + +import java.time.Duration; +import java.util.concurrent.CompletionStage; + +public final class GlobalManager { + private final Duration askTimeout = Duration.ofHours(5); + + private final ClusterSharding clusterSharding; + + public GlobalManager(final ClusterSharding clusterSharding) { + this.clusterSharding = clusterSharding; + this.clusterSharding.init( + Entity.of( + GlobalRegistration.ENTITY_TYPE_KEY, + ctx -> GlobalRegistration.create( + ctx.getEntityId(), + PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()) + ) + ) + ); + } + + + public CompletionStage registerGroup( + GroupRegistration.Response.GroupRegistered registered + ) { + final Group group = registered.group(); + return this.getGlobalEntity() + .ask(replyTo -> new GlobalCommand.RegisterGroup(replyTo, group), this.askTimeout) + .thenApply(notUsed -> registered); + } + + + private EntityRef getGlobalEntity() { + return this.clusterSharding.entityRefFor(GlobalRegistration.ENTITY_TYPE_KEY, "global"); + } + + public CompletionStage getGroups() { + return this.getGlobalEntity().ask(GlobalCommand.GetGroups::new, this.askTimeout); + } +} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupCommand.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupCommand.java index ca84c172..b73d04e0 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupCommand.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupCommand.java @@ -25,161 +25,41 @@ package org.spongepowered.downloads.artifact.group; import akka.actor.typed.ActorRef; -import com.fasterxml.jackson.annotation.JsonCreator; import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; 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 java.util.Objects; - public interface GroupCommand extends Jsonable { - final class GetGroup implements GroupCommand { - public final String groupId; - public final ActorRef replyTo; - - public GetGroup(final String groupId, final ActorRef replyTo) { - this.groupId = groupId; - this.replyTo = replyTo; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (GetGroup) obj; - return Objects.equals(this.groupId, that.groupId); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId); - } - - @Override - public String toString() { - return "GetGroup[" + - "groupId=" + this.groupId + ']'; - } + record GetGroup( + String groupId, + ActorRef replyTo + ) implements GroupCommand { } - final class GetArtifacts implements GroupCommand { - public final String groupId; - public final ActorRef replyTo; - - public GetArtifacts(final String groupId, final ActorRef replyTo) { - this.groupId = groupId; - this.replyTo = replyTo; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (GetArtifacts) obj; - return Objects.equals(this.groupId, that.groupId); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId); - } - - @Override - public String toString() { - return "GetArtifacts[" + - "groupId=" + this.groupId + ']'; - } + record GetArtifacts( + String groupId, + ActorRef replyTo + ) implements GroupCommand { } - final class RegisterArtifact implements GroupCommand { - public final String artifact; - public final ActorRef replyTo; - - @JsonCreator - public RegisterArtifact( - final String artifact, final ActorRef replyTo - ) { - this.artifact = artifact; - this.replyTo = replyTo; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (RegisterArtifact) obj; - return Objects.equals(this.artifact, that.artifact); - } - - @Override - public int hashCode() { - return Objects.hash(this.artifact); - } - - @Override - public String toString() { - return "RegisterArtifact[" + - "artifact=" + this.artifact + ']'; - } + record RegisterArtifact( + String artifact, + ActorRef replyTo + ) implements GroupCommand { } - final class RegisterGroup implements GroupCommand { - public final String mavenCoordinates; - public final String name; - public final String website; - public final ActorRef replyTo; - - public RegisterGroup(final String mavenCoordinates, final String name, final String website, final ActorRef replyTo) { - this.mavenCoordinates = mavenCoordinates; - this.name = name; - this.website = website; - this.replyTo = replyTo; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (RegisterGroup) obj; - return Objects.equals(this.mavenCoordinates, that.mavenCoordinates) && - Objects.equals(this.name, that.name) && - Objects.equals(this.website, that.website); - } - - @Override - public int hashCode() { - return Objects.hash(this.mavenCoordinates, this.name, this.website); - } - - @Override - public String toString() { - return "RegisterGroup[" + - "mavenCoordinates=" + this.mavenCoordinates + ", " + - "name=" + this.name + ", " + - "website=" + this.website + ']'; - } + record RegisterGroup( + String mavenCoordinates, + String name, + String website, + ActorRef replyTo + ) implements GroupCommand { } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEntity.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEntity.java index 7f9ae120..756b77f8 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEntity.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEntity.java @@ -106,8 +106,8 @@ private GroupState handleRegistration( private GroupState handleArtifactRegistration( final PopulatedState state, final GroupEvent.ArtifactRegistered event ) { - final var add = state.artifacts.add(event.artifact); - return new PopulatedState(state.groupCoordinates, state.name, state.website, add); + final var add = state.artifacts().add(event.artifact()); + return new PopulatedState(state.groupCoordinates(), state.name(), state.website(), add); } @Override @@ -116,12 +116,19 @@ public CommandHandlerWithReply commandHand builder.forState(GroupState::isEmpty) .onCommand(GroupCommand.RegisterGroup.class, this::respondToRegisterGroup) - .onCommand(GroupCommand.RegisterArtifact.class, (state, cmd) -> this.Effect().reply(cmd.replyTo, new ArtifactRegistration.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))) - ; + .onCommand(GroupCommand.RegisterArtifact.class, (state, cmd) -> + this.Effect().reply(cmd.replyTo(), new ArtifactRegistration.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.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); @@ -130,7 +137,7 @@ public CommandHandlerWithReply commandHand @Override public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(1, 2); + return RetentionCriteria.snapshotEvery(5, 2); } @Override @@ -143,9 +150,9 @@ private ReplyEffect respondToRegisterGroup( final GroupCommand.RegisterGroup cmd ) { return this.Effect() - .persist(new GroupEvent.GroupRegistered(cmd.mavenCoordinates, cmd.name, cmd.website)) + .persist(new GroupEvent.GroupRegistered(cmd.mavenCoordinates(), cmd.name(), cmd.website())) .thenReply( - cmd.replyTo, + cmd.replyTo(), newState -> new GroupRegistration.Response.GroupRegistered( new Group( newState.groupCoordinates(), @@ -159,36 +166,37 @@ private ReplyEffect respondToRegisterArtifact( final PopulatedState state, final GroupCommand.RegisterArtifact cmd ) { - if (state.artifacts.contains(cmd.artifact)) { - this.Effect().reply(cmd.replyTo, new ArtifactRegistration.Response.ArtifactAlreadyRegistered( - cmd.artifact, - state.groupCoordinates + if (state.artifacts().contains(cmd.artifact())) { + this.Effect().reply(cmd.replyTo(), new ArtifactRegistration.Response.ArtifactAlreadyRegistered( + cmd.artifact(), + state.groupCoordinates() )); } final var group = state.asGroup(); - final var coordinates = new ArtifactCoordinates(group.groupCoordinates, cmd.artifact); + final var coordinates = new ArtifactCoordinates(group.groupCoordinates(), cmd.artifact()); final EffectFactories effect = this.Effect(); - return effect.persist(new GroupEvent.ArtifactRegistered(state.groupCoordinates, cmd.artifact)) - .thenReply(cmd.replyTo, (s) -> new ArtifactRegistration.Response.ArtifactRegistered(coordinates)); + return effect.persist(new GroupEvent.ArtifactRegistered(state.groupCoordinates(), cmd.artifact())) + .thenReply(cmd.replyTo(), (s) -> new ArtifactRegistration.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(() -> new URL(website)) + final String website = state.website(); + return this.Effect().reply(cmd.replyTo(), Try.of(() -> new URL(website)) .mapTry(url -> { - final Group group = new Group(state.groupCoordinates, state.name, website); + final Group group = new Group(state.groupCoordinates(), state.name(), website); return new GroupResponse.Available(group); }) - .getOrElseGet(throwable -> new GroupResponse.Missing(cmd.groupId))); + .getOrElseGet(throwable -> 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())); + return this.Effect().reply( + cmd.replyTo(), new GetArtifactsResponse.ArtifactsAvailable(state.artifacts().toList())); } } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEvent.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEvent.java index ed0c4075..a3819bfb 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEvent.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupEvent.java @@ -34,10 +34,10 @@ import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; import java.io.Serial; import java.util.Objects; -import java.util.StringJoiner; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -106,52 +106,19 @@ public String toString() { } @JsonTypeName("artifact-registered") - @JsonDeserialize - final class ArtifactRegistered implements GroupEvent { - - @Serial private static final long serialVersionUID = 6319289932327553919L; - - public final String groupId; - public final String artifact; - - @JsonCreator - public ArtifactRegistered(final String groupId, final String artifact) { - this.groupId = groupId; - this.artifact = artifact; + @JsonDeserialize + record ArtifactRegistered( + String groupId, + String artifact + ) implements GroupEvent { + + public ArtifactCoordinates coordinates() { + return new ArtifactCoordinates(this.groupId, this.artifact); } - @Override - public String groupId() { - return this.groupId; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final ArtifactRegistered that = (ArtifactRegistered) o; - return Objects.equals(this.groupId, that.groupId) && Objects.equals(this.artifact, that.artifact); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.artifact); - } - - @Override - public String toString() { - return new StringJoiner( - ", ", - ArtifactRegistered.class.getSimpleName() + "[", - "]" - ) - .add("groupId='" + this.groupId + "'") - .add("artifact='" + this.artifact + "'") - .toString(); + @JsonCreator + public ArtifactRegistered { } } + } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java new file mode 100644 index 00000000..e7eb24e2 --- /dev/null +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java @@ -0,0 +1,96 @@ +package org.spongepowered.downloads.artifact.group; + +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.cluster.sharding.typed.javadsl.EntityRef; +import com.lightbend.lagom.javadsl.api.transport.NotFound; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +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.details.DetailsManager; +import org.spongepowered.downloads.artifact.global.GlobalManager; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public final class GroupManager { + private final ClusterSharding clusterSharding; + private final GlobalManager global; + private final Duration askTimeout = Duration.ofHours(5); + private final DetailsManager details; + + public GroupManager(ClusterSharding clusterSharding, final DetailsManager details, final GlobalManager global) { + this.clusterSharding = clusterSharding; + this.global = global; + this.details = details; + this.clusterSharding.init( + Entity.of( + GroupEntity.ENTITY_TYPE_KEY, + GroupEntity::create + ) + ); + } + + public CompletionStage registerGroup( + GroupRegistration.RegisterGroupRequest registration + ) { + final String mavenCoordinates = registration.groupCoordinates; + final String name = registration.name; + final String website = registration.website; + return this.groupEntity(registration.groupCoordinates.toLowerCase(Locale.ROOT)) + .ask( + replyTo -> new GroupCommand.RegisterGroup(mavenCoordinates, name, website, replyTo), + this.askTimeout + ).thenCompose(response -> { + if (!(response instanceof GroupRegistration.Response.GroupRegistered registered)) { + return CompletableFuture.completedFuture(response); + } + return this.global.registerGroup(registered); + + }); + } + + + private EntityRef groupEntity(final String groupId) { + return this.clusterSharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, groupId.toLowerCase(Locale.ROOT)); + } + + public CompletionStage registerArtifact( + ArtifactRegistration.RegisterArtifact reg, String groupId + ) { + final var sanitizedGroupId = groupId.toLowerCase(Locale.ROOT); + return this.groupEntity(sanitizedGroupId) + .ask( + replyTo -> new GroupCommand.RegisterArtifact(reg.artifactId(), replyTo), this.askTimeout) + .thenCompose(response -> switch (response) { + case ArtifactRegistration.Response.GroupMissing missing -> + throw new NotFound(String.format("group %s does not exist", missing.s())); + + case ArtifactRegistration.Response.ArtifactRegistered registered -> + this.details.registerArtifact(registered, reg.displayName()); + + default -> CompletableFuture.completedFuture(response); + }); + } + + public CompletionStage getArtifacts(String groupId) { + return this.groupEntity(groupId) + .ask(replyTo -> new GroupCommand.GetArtifacts(groupId, replyTo), this.askTimeout) + .thenApply(response -> switch (response) { + case GetArtifactsResponse.GroupMissing m -> throw new NotFound(String.format("group '%s' not found", m.groupRequested())); + case GetArtifactsResponse.ArtifactsAvailable a -> a; + }); + } + + public CompletionStage get(String groupId) { + return this.groupEntity(groupId.toLowerCase(Locale.ROOT)) + .ask(replyTo -> new GroupCommand.GetGroup(groupId, replyTo), this.askTimeout) + .thenApply(response -> switch (response) { + case GroupResponse.Missing m -> throw new NotFound(String.format("group '%s' not found", m.groupId())); + case GroupResponse.Available a -> a; + }); + } +} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/EmptyState.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/EmptyState.java index c8ef7bad..07145f15 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/EmptyState.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/EmptyState.java @@ -24,9 +24,17 @@ */ package org.spongepowered.downloads.artifact.group.state; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.spongepowered.downloads.artifact.api.Group; -public final class EmptyState implements GroupState { +@JsonDeserialize +public record EmptyState() implements GroupState { + + @JsonCreator + public EmptyState { + } + @Override public boolean isEmpty() { return true; diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/GroupState.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/GroupState.java index 0354ab07..6de32365 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/GroupState.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/GroupState.java @@ -24,9 +24,17 @@ */ package org.spongepowered.downloads.artifact.group.state; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.Group; -public interface GroupState { +@JsonDeserialize +@JsonSubTypes({ + @JsonSubTypes.Type(value = PopulatedState.class, name = "populated"), + @JsonSubTypes.Type(value = EmptyState.class, name = "empty") +}) +public interface GroupState extends Jsonable { boolean isEmpty(); diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/PopulatedState.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/PopulatedState.java index 4c838822..1c16b790 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/PopulatedState.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/state/PopulatedState.java @@ -31,42 +31,21 @@ import org.spongepowered.downloads.artifact.api.Group; @JsonDeserialize -public class PopulatedState implements GroupState, CompressedJsonable { - public final String groupCoordinates; - public final String name; - public final String website; - public final Set artifacts; - +public record PopulatedState( + String groupCoordinates, + String name, + String website, + Set artifacts +) implements GroupState, CompressedJsonable { @JsonCreator - public PopulatedState( - final String groupCoordinates, final String name, final String website, final Set artifacts - ) { - this.groupCoordinates = groupCoordinates; - this.name = name; - this.website = website; - this.artifacts = artifacts; + public PopulatedState { } public boolean isEmpty() { - return this.groupCoordinates.isEmpty() || this.name.isEmpty(); + return this.groupCoordinates().isEmpty() || this.name().isEmpty(); } public Group asGroup() { - return new Group(this.groupCoordinates, this.name, this.website); - } - - @Override - public String website() { - return this.website; - } - - @Override - public String name() { - return this.name; - } - - @Override - public String groupCoordinates() { - return this.groupCoordinates; + return new Group(this.groupCoordinates(), this.name(), this.website()); } } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/readside/ArtifactReadside.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/readside/ArtifactReadside.java index 76c258f1..220762f5 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/readside/ArtifactReadside.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/readside/ArtifactReadside.java @@ -85,15 +85,15 @@ private JpaArtifact findOrRegisterArtifact( "Artifact.findById", JpaArtifact.class ); - return artifactQuery.setParameter("groupId", coordinates.groupId) - .setParameter("artifactId", coordinates.artifactId) + 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); + jpaArtifact.setGroupId(coordinates.groupId()); + jpaArtifact.setArtifactId(coordinates.artifactId()); em.persist(jpaArtifact); return jpaArtifact; }); diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/transport/RestArtifactService.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/transport/RestArtifactService.java new file mode 100644 index 00000000..3cc63e47 --- /dev/null +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/transport/RestArtifactService.java @@ -0,0 +1,212 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.transport; + +import akka.NotUsed; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.japi.Pair; +import com.google.inject.Inject; +import com.lightbend.lagom.javadsl.api.ServiceCall; +import com.lightbend.lagom.javadsl.api.broker.Topic; +import com.lightbend.lagom.javadsl.api.transport.NotFound; +import com.lightbend.lagom.javadsl.broker.TopicProducer; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; +import com.lightbend.lagom.javadsl.persistence.Offset; +import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; +import com.lightbend.lagom.javadsl.server.ServerServiceCall; +import io.vavr.control.Try; +import org.pac4j.core.config.Config; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.ArtifactService; +import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; +import org.spongepowered.downloads.artifact.api.event.GroupUpdate; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +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.query.GroupsResponse; +import org.spongepowered.downloads.artifact.details.DetailsEvent; +import org.spongepowered.downloads.artifact.details.DetailsManager; +import org.spongepowered.downloads.artifact.global.GlobalManager; +import org.spongepowered.downloads.artifact.group.GroupEvent; +import org.spongepowered.downloads.artifact.group.GroupManager; +import org.spongepowered.downloads.auth.AuthenticatedInternalService; +import org.spongepowered.downloads.auth.SOADAuth; +import org.spongepowered.downloads.auth.api.utils.AuthUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +public class RestArtifactService implements ArtifactService, + AuthenticatedInternalService { + private final Config securityConfig; + private final PersistentEntityRegistry persistentEntityRegistry; + private final AuthUtils auth; + private final DetailsManager details; + private final GroupManager group; + private final GlobalManager global; + + @Inject + public RestArtifactService( + final ClusterSharding clusterSharding, + final PersistentEntityRegistry persistentEntityRegistry, + final AuthUtils auth, + @SOADAuth final Config securityConfig + ) { + this.securityConfig = securityConfig; + this.auth = auth; + this.details = new DetailsManager(clusterSharding); + this.global = new GlobalManager(clusterSharding); + this.group = new GroupManager(clusterSharding, this.details, this.global); + + this.persistentEntityRegistry = persistentEntityRegistry; + } + + @Override + public ServiceCall getArtifacts(final String groupId) { + return none -> this.group.getArtifacts(groupId); + } + + @Override + public ServiceCall registerGroup() { + return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> + this.group::registerGroup); + } + + @Override + public ServiceCall, ArtifactDetails.Response> updateDetails( + final String groupId, + final String artifactId + ) { + return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> update -> { + final var coords = RestArtifactService.validateCoordinates(groupId, artifactId).get(); + + return switch (update) { + case ArtifactDetails.Update.Website w -> this.details.UpdateWebsite(coords, w); + case ArtifactDetails.Update.DisplayName d -> this.details.UpdateDisplayName(coords, d); + case ArtifactDetails.Update.GitRepository gr -> this.details.UpdateGitRepository(coords, gr); + case ArtifactDetails.Update.Issues i -> this.details.UpdateIssues(coords, i); + }; + }); + } + + @Override + public ServerServiceCall registerArtifacts( + final String groupId + ) { + return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, p -> reg -> this.group.registerArtifact(reg, groupId)); + } + + @Override + public ServiceCall getGroup(final String groupId) { + return notUsed -> this.group.get(groupId); + } + + @Override + public ServiceCall getGroups() { + return notUsed -> this.global.getGroups(); + } + + @Override + public Topic groupTopic() { + return TopicProducer.taggedStreamWithOffset( + GroupEvent.TAG.allTags(), + (AggregateEventTag aggregateTag, Offset fromOffset) -> + this.persistentEntityRegistry.eventStream(aggregateTag, fromOffset) + .mapConcat(RestArtifactService::convertEvent) + ); + } + + @Override + public Topic artifactUpdate() { + return TopicProducer.taggedStreamWithOffset( + DetailsEvent.TAG.allTags(), + (AggregateEventTag tag, Offset from) -> + this.persistentEntityRegistry.eventStream(tag, from) + .mapConcat(RestArtifactService::convertDetailsEvent) + ); + } + + private static List> convertEvent(Pair pair) { + return switch (pair.first()) { + case GroupEvent.ArtifactRegistered a -> Collections.singletonList( + Pair.create( + new GroupUpdate.ArtifactRegistered(a.coordinates()), + pair.second() + )); + case GroupEvent.GroupRegistered g -> Collections.singletonList( + Pair.create(new GroupUpdate.GroupRegistered(g.groupId, g.name, g.website), pair.second())); + default -> Collections.emptyList(); + }; + } + + private static List> convertDetailsEvent(Pair pair) { + final ArtifactUpdate message; + return switch (pair.first()) { + case DetailsEvent.ArtifactGitRepositoryUpdated repoUpdated: + message = new ArtifactUpdate.GitRepositoryAssociated(repoUpdated.coordinates(), repoUpdated.gitRepo()); + yield Collections.singletonList(Pair.create(message, pair.second())); + case DetailsEvent.ArtifactRegistered registered: + message = new ArtifactUpdate.ArtifactRegistered(registered.coordinates()); + yield Collections.singletonList(Pair.create(message, pair.second())); + case DetailsEvent.ArtifactDetailsUpdated details: + message = new ArtifactUpdate.DisplayNameUpdated(details.coordinates(), details.displayName()); + yield Collections.singletonList(Pair.create(message, pair.second())); + case DetailsEvent.ArtifactIssuesUpdated issues: + message = new ArtifactUpdate.IssuesUpdated(issues.coordinates(), issues.url()); + yield Collections.singletonList(Pair.create(message, pair.second())); + case DetailsEvent.ArtifactWebsiteUpdated website: + message = new ArtifactUpdate.WebsiteUpdated(website.coordinates(), website.url()); + yield Collections.singletonList(Pair.create(message, pair.second())); + default: + yield Collections.emptyList(); + }; + } + + @Override + public Config getSecurityConfig() { + return this.securityConfig; + } + + @Override + public AuthUtils auth() { + return this.auth; + } + + private static Try validateCoordinates(final String groupId, final String artifactId) { + final var validGroupId = groupId.toLowerCase(Locale.ROOT).trim(); + final var validArtifactId = artifactId.toLowerCase(Locale.ROOT).trim(); + if (validGroupId.isEmpty()) { + return Try.failure(new NotFound("group not found")); + } + if (validArtifactId.isEmpty()) { + return Try.failure(new NotFound("artifact not found")); + } + final var coords = new ArtifactCoordinates(validGroupId, validArtifactId); + return Try.success(coords); + } +} diff --git a/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/akka/EventBehaviorTestkit.java b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/akka/EventBehaviorTestkit.java new file mode 100644 index 00000000..290a106c --- /dev/null +++ b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/akka/EventBehaviorTestkit.java @@ -0,0 +1,27 @@ +package org.spongepowered.downloads.artifact.test.akka; + +import akka.actor.testkit.typed.javadsl.TestKitJunitResource; +import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; +import com.typesafe.config.ConfigFactory; + +public class EventBehaviorTestkit { + public static TestKitJunitResource createTestKit() { + return new TestKitJunitResource(ConfigFactory.parseString( + """ + akka.serialization.jackson { + # The Jackson JSON serializer will register these modules. + jackson-modules += "akka.serialization.jackson.AkkaJacksonModule" + jackson-modules += "akka.serialization.jackson.AkkaTypedJacksonModule" + # AkkaStreamsModule optionally included if akka-streams is in classpath + jackson-modules += "akka.serialization.jackson.AkkaStreamJacksonModule" + jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule" + jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module" + jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule" + jackson-modules += "io.vavr.jackson.datatype.VavrModule" + } + """ + ) + .resolve() // Resolve the config first + .withFallback(EventSourcedBehaviorTestKit.config())); + } +} diff --git a/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/global/GlobalArtifactsTest.java b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/global/GlobalArtifactsTest.java index 25488891..ff4435a7 100644 --- a/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/global/GlobalArtifactsTest.java +++ b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/global/GlobalArtifactsTest.java @@ -28,7 +28,6 @@ import akka.actor.testkit.typed.javadsl.TestKitJunitResource; import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; import akka.persistence.typed.PersistenceId; -import com.typesafe.config.ConfigFactory; import io.vavr.collection.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -40,29 +39,14 @@ import org.spongepowered.downloads.artifact.global.GlobalEvent; import org.spongepowered.downloads.artifact.global.GlobalRegistration; import org.spongepowered.downloads.artifact.global.GlobalState; +import org.spongepowered.downloads.artifact.test.akka.EventBehaviorTestkit; @TestInstance(TestInstance.Lifecycle.PER_CLASS) public class GlobalArtifactsTest implements BeforeEachCallback { // If actor refs are being used, the testkit needs to have the akka modules // locally. - public static final TestKitJunitResource testkit = new TestKitJunitResource(ConfigFactory.parseString( - """ - akka.serialization.jackson { - # The Jackson JSON serializer will register these modules. - jackson-modules += "akka.serialization.jackson.AkkaJacksonModule" - jackson-modules += "akka.serialization.jackson.AkkaTypedJacksonModule" - # AkkaStreamsModule optionally included if akka-streams is in classpath - jackson-modules += "akka.serialization.jackson.AkkaStreamJacksonModule" - jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule" - jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module" - jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule" - jackson-modules += "io.vavr.jackson.datatype.VavrModule" - } - """ - ) - .resolve() // Resolve the config first - .withFallback(EventSourcedBehaviorTestKit.config())); + public static final TestKitJunitResource testkit = EventBehaviorTestkit.createTestKit(); private final EventSourcedBehaviorTestKit behaviorKit = EventSourcedBehaviorTestKit.create( testkit.system(), GlobalRegistration.create( diff --git a/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/groups/GroupEntityCommandsTest.java b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/groups/GroupEntityCommandsTest.java new file mode 100644 index 00000000..e3a8a115 --- /dev/null +++ b/artifact-impl/src/test/java/org/spongepowered/downloads/artifact/test/groups/GroupEntityCommandsTest.java @@ -0,0 +1,126 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.test.groups; + +import akka.actor.testkit.typed.javadsl.TestKitJunitResource; +import akka.cluster.sharding.typed.javadsl.EntityContext; +import akka.cluster.sharding.typed.javadsl.EntityTypeKey; +import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; +import io.vavr.collection.List; +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.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.group.GroupCommand; +import org.spongepowered.downloads.artifact.group.GroupEntity; +import org.spongepowered.downloads.artifact.group.GroupEvent; +import org.spongepowered.downloads.artifact.group.state.GroupState; +import org.spongepowered.downloads.artifact.test.akka.EventBehaviorTestkit; + +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +public class GroupEntityCommandsTest implements BeforeEachCallback { + + // If actor refs are being used, the testkit needs to have the akka modules + // locally. + public static final TestKitJunitResource testkit = EventBehaviorTestkit.createTestKit(); + + private final EventSourcedBehaviorTestKit behaviorKit = EventSourcedBehaviorTestKit.create( + testkit.system(), GroupEntity.create( + new EntityContext<>( + EntityTypeKey.create(GroupCommand.class, "GroupEntity"), + "org.spongepowered", + null + ))); + + @Override + public void beforeEach(final ExtensionContext context) { + this.behaviorKit.clear(); + } + + @Test + public void verifyGroupRegistration() { + final var unhandledMessageProbe = testkit.createUnhandledMessageProbe(); + final var spongePowered = new Group("org.spongepowered", "SpongePowered", "https://spongepowered.org/"); + final var result = behaviorKit.runCommand( + replyTo -> new GroupCommand.RegisterGroup( + spongePowered.groupCoordinates(), spongePowered.name(), spongePowered.website(), replyTo)); + Assertions.assertEquals(new GroupRegistration.Response.GroupRegistered(spongePowered), result.reply()); + unhandledMessageProbe.expectNoMessage(); + } + + @Test + public void verify404GroupAvailable() { + final var unhandledMessageProbe = testkit.createUnhandledMessageProbe(); + final var spongePowered = new Group("org.spongepowered", "SpongePowered", "https://spongepowered.org/"); + final var result = behaviorKit.runCommand( + replyTo -> new GroupCommand.GetGroup(spongePowered.groupCoordinates(), replyTo)); + Assertions.assertEquals(new GroupResponse.Missing("org.spongepowered"), result.reply()); + unhandledMessageProbe.expectNoMessage(); + } + + private Group setUpGroup() { + final var spongePowered = new Group("org.spongepowered", "SpongePowered", "https://spongepowered.org/"); + behaviorKit.runCommand( + replyTo -> new GroupCommand.RegisterGroup( + spongePowered.groupCoordinates(), spongePowered.name(), spongePowered.website(), replyTo)); + return spongePowered; + } + + @Test + public void verify200GroupAvailable() { + final var unhandledMessageProbe = testkit.createUnhandledMessageProbe(); + final var spongePowered = this.setUpGroup(); + final var result = behaviorKit.runCommand( + replyTo -> new GroupCommand.GetGroup(spongePowered.groupCoordinates(), replyTo)); + Assertions.assertEquals(new GroupResponse.Available(spongePowered), result.reply()); + unhandledMessageProbe.expectNoMessage(); + } + + @Test + public void verify200ArtifactsAvailable() { + final var unhandledMessageProbe = testkit.createUnhandledMessageProbe(); + final var spongePowered = this.setUpGroup(); + final var result = behaviorKit.runCommand( + replyTo -> new GroupCommand.GetArtifacts(spongePowered.groupCoordinates(), replyTo)); + Assertions.assertEquals(new GetArtifactsResponse.ArtifactsAvailable(List.empty()), result.reply()); + unhandledMessageProbe.expectNoMessage(); + } + + @Test + public void verify404ArtifactsUnavailable() { + final var unhandledMessageProbe = testkit.createUnhandledMessageProbe(); + final var spongePowered = new Group("org.spongepowered", "SpongePowered", "https://spongepowered.org/"); + final var result = behaviorKit.runCommand( + replyTo -> new GroupCommand.GetArtifacts(spongePowered.groupCoordinates(), replyTo)); + Assertions.assertEquals(new GetArtifactsResponse.GroupMissing("org.spongepowered"), result.reply()); + unhandledMessageProbe.expectNoMessage(); + } + +} diff --git a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java b/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java index c04cccae..5113f526 100644 --- a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java +++ b/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java @@ -31,47 +31,23 @@ import io.vavr.collection.SortedSet; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import java.util.Objects; -import java.util.StringJoiner; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = GetArtifactDetailsResponse.RetrievedArtifact.class, name = "latest") }) -public interface GetArtifactDetailsResponse { +public sealed interface GetArtifactDetailsResponse { @JsonSerialize - record RetrievedArtifact(ArtifactCoordinates coordinates, - String displayName, String website, String gitRepository, - String issues, - Map> tags) implements GetArtifactDetailsResponse { - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RetrievedArtifact that = (RetrievedArtifact) o; - return Objects.equals(coordinates, that.coordinates) && Objects.equals( - displayName, that.displayName); - } - - @Override - public int hashCode() { - return Objects.hash(coordinates, displayName); - } + record RetrievedArtifact( + ArtifactCoordinates coordinates, + String displayName, + String website, + String gitRepository, + String issues, + Map> tags + ) implements GetArtifactDetailsResponse { - @Override - public String toString() { - return new StringJoiner(", ", RetrievedArtifact.class.getSimpleName() + "[", "]") - .add("coordinates=" + coordinates) - .add("displayName='" + displayName + "'") - .toString(); - } } } diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java index e7857a12..404430c3 100644 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java +++ b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java @@ -125,7 +125,7 @@ private static Source parseResponseIntoArtifacts( ) { final Source groups; if (r instanceof GroupsResponse.Available a) { - groups = Source.from(a.groups.map(Group::getGroupCoordinates)); + groups = Source.from(a.groups().map(Group::groupCoordinates)); } else { groups = Source.empty(); } diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java index db162da3..62af456e 100644 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java +++ b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java @@ -239,7 +239,7 @@ private static Either, Path> cloneRepo( final URI remoteRepo ) { final var tempdirPrefix = String.format( - "soad-%s-%s", coordinates.artifactId, + "soad-%s-%s", coordinates.artifactId(), UUID.randomUUID() ); final var repoDirectory = Try.of(() -> Files.createTempDirectory( diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java index 02805524..5251200a 100644 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java +++ b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java @@ -124,11 +124,11 @@ private static Behavior awaiting( .onMessage(PerformResync.class, g -> { final var makeRequest = artifactService.getGroups() .invoke() - .thenApply(groups -> ((GroupsResponse.Available) groups).groups) + .thenApply(groups -> ((GroupsResponse.Available) groups).groups()) .thenCompose(groups -> { final Sink, CompletionStage>> fold = Sink.fold( List.empty(), List::appendAll); - return Source.from(groups.map(Group::getGroupCoordinates).asJava()) + return Source.from(groups.map(Group::groupCoordinates).asJava()) .async() .via(requestFlow) .map(RequestArtifactsToSync.ArtifactsToSync::artifactsNeeded) diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java index ad4bb94f..7bb8d69d 100644 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java +++ b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java @@ -135,11 +135,11 @@ private ReplyEffect handleResponse(SyncState state, private ReplyEffect handleResync(SyncState state, Command.Resync cmd) { - final var groupId = !state.groupId.equals(cmd.coordinates().groupId) - ? cmd.coordinates().groupId + final var groupId = !state.groupId.equals(cmd.coordinates().groupId()) + ? cmd.coordinates().groupId() : state.groupId; - final var artifactId = !state.artifactId.equals(cmd.coordinates().artifactId) - ? cmd.coordinates().artifactId + final var artifactId = !state.artifactId.equals(cmd.coordinates().artifactId()) + ? cmd.coordinates().artifactId() : state.artifactId; ctx.pipeToSelf( getArtifactMetadata(groupId, artifactId), diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java index 0a9ddfe9..7b796485 100644 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java +++ b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java @@ -83,14 +83,14 @@ public ReadSideHandler buildHandler() { "Artifact.selectByGroupAndArtifact", JpaArtifact.class ); - final var singleResult = artifactQuery.setParameter("groupId", coordinates.groupId) - .setParameter("artifactId", coordinates.artifactId) + final var singleResult = artifactQuery.setParameter("groupId", coordinates.groupId()) + .setParameter("artifactId", coordinates.artifactId()) .setMaxResults(1) .getResultList(); if (singleResult.isEmpty()) { final var jpaArtifact = new JpaArtifact(); - jpaArtifact.setGroupId(coordinates.groupId); - jpaArtifact.setArtifactId(coordinates.artifactId); + jpaArtifact.setGroupId(coordinates.groupId()); + jpaArtifact.setArtifactId(coordinates.artifactId()); em.persist(jpaArtifact); } }) @@ -129,8 +129,8 @@ public ReadSideHandler buildHandler() { "Artifact.selectWithTags", JpaArtifact.class ); - artifactQuery.setParameter("groupId", coordinates.groupId); - artifactQuery.setParameter("artifactId", coordinates.artifactId); + artifactQuery.setParameter("groupId", coordinates.groupId()); + artifactQuery.setParameter("artifactId", coordinates.artifactId()); final var tag = tagRegistered.entry(); final var artifact = artifactQuery.getSingleResult(); final var jpaTag = artifact.getTags().stream() @@ -153,8 +153,8 @@ public ReadSideHandler buildHandler() { JpaArtifact.class ); - artifactQuery.setParameter("groupId", coordinates.groupId); - artifactQuery.setParameter("artifactId", coordinates.artifactId); + artifactQuery.setParameter("groupId", coordinates.groupId()); + artifactQuery.setParameter("artifactId", coordinates.artifactId()); final var artifact = artifactQuery.getSingleResult(); final var recommendation = em.createNamedQuery( "RegexRecommendation.findByArtifact", JpaArtifactRegexRecommendation.class) @@ -178,8 +178,8 @@ public ReadSideHandler buildHandler() { "ArtifactVersion.findByCoordinates", JpaArtifactVersion.class ) - .setParameter("groupId", event.coordinates().groupId) - .setParameter("artifactId", event.coordinates().artifactId) + .setParameter("groupId", event.coordinates().groupId()) + .setParameter("artifactId", event.coordinates().artifactId()) .setParameter("version", v) .setMaxResults(1) .getSingleResult(); diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java index 978f7cb9..204e0cdf 100644 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java +++ b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java @@ -127,8 +127,8 @@ private static Behavior waiting( final int rowsAffected = data.refreshRecommendations .map(coordinates -> em.createNativeQuery( "select version.refreshVersionRecommendations(:artifactId, :groupId)") - .setParameter("artifactId", coordinates.artifactId) - .setParameter("groupId", coordinates.groupId) + .setParameter("artifactId", coordinates.artifactId()) + .setParameter("groupId", coordinates.groupId()) .getSingleResult()) .sum().intValue(); return new Completed(data, updatedVersionedTags, rowsAffected); diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java index 7baf49a0..1a4580ec 100644 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java +++ b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java @@ -229,8 +229,8 @@ private static QueryVersions.VersionInfo getUntaggedVersions( .setParameter("recommended", isRecommended) ) .orElseGet(() -> em.createNamedQuery("VersionedArtifactView.count", Long.class)) - .setParameter("groupId", query.coordinates.groupId) - .setParameter("artifactId", query.coordinates.artifactId) + .setParameter("groupId", query.coordinates.groupId()) + .setParameter("artifactId", query.coordinates.artifactId()) .getSingleResult().intValue(); if (totalCount <= 0) { throw new NotFound("group or artifact not found"); @@ -245,8 +245,8 @@ private static QueryVersions.VersionInfo getUntaggedVersions( .orElseGet(() -> em.createNamedQuery( "VersionedArtifactView.findByArtifact", JpaVersionedArtifactView.class )) - .setParameter("groupId", query.coordinates.groupId) - .setParameter("artifactId", query.coordinates.artifactId) + .setParameter("groupId", query.coordinates.groupId()) + .setParameter("artifactId", query.coordinates.artifactId()) .setMaxResults(query.offset + query.limit) .getResultList(); final var mappedByCoordinates = untaggedVersions.stream() @@ -277,8 +277,8 @@ private static QueryVersions.VersionInfo getTaggedVersions( .orElseGet(() -> em.createNamedQuery( "TaggedVersion.findAllMatchingTagValues", JpaTaggedVersion.class )) - .setParameter("groupId", query.coordinates.groupId) - .setParameter("artifactId", query.coordinates.artifactId) + .setParameter("groupId", query.coordinates.groupId()) + .setParameter("artifactId", query.coordinates.artifactId()) .setParameter("tagName", tag.tagName) .setParameter("tagValue", tag.tagValue + "%") .getResultStream() From e9684545e36df467828e143373b388eacf666499 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Thu, 8 Sep 2022 10:25:12 +0200 Subject: [PATCH 2/9] Start rebuilding with pure Akka --- .../artifact/api/query/GroupRegistration.java | 62 +--- .../artifact/api/query/GroupsResponse.java | 1 - .../artifact/group/GroupManager.java | 8 +- build.sbt | 61 +++- docs/Dockerfile | 8 + docs/project/build.properties | 1 + docs/systemofadownload.yaml | 324 ++++++++++++++++++ .../main/java/com/example/QuickstartApp.java | 62 ++++ .../main/java/com/example/UserRegistry.java | 101 ++++++ .../src/main/java/com/example/UserRoutes.java | 112 ++++++ downloads-api/src/main/java/module-info.java | 17 + .../spongepowered/downloads/api/Artifact.java | 46 +++ .../downloads/api/ArtifactCollection.java | 42 +++ .../downloads/api/ArtifactCoordinates.java | 66 ++++ .../spongepowered/downloads/api/Group.java | 42 +++ .../downloads/api/MavenCoordinates.java | 192 +++++++++++ .../downloads/app/SystemOfADownloadsApp.java | 52 +++ .../downloads/artifacts/ArtifactQueries.java | 52 +++ .../downloads/artifacts/ArtifactRoutes.java | 89 +++++ .../artifacts/models/JpaArtifact.java | 181 ++++++++++ .../artifacts/models/JpaArtifactTagValue.java | 137 ++++++++ .../artifacts/transport/ArtifactDetails.java | 128 +++++++ .../transport/ArtifactRegistration.java | 82 +++++ .../transport/GetArtifactDetailsResponse.java | 49 +++ .../transport/GetArtifactsResponse.java | 40 +++ .../transport/GroupRegistration.java | 56 +++ .../artifacts/transport/GroupResponse.java | 36 ++ .../artifacts/transport/GroupsResponse.java | 38 ++ .../downloads/routes/VersionRoutes.java | 4 + .../downloads/versions/VersionQueries.java | 15 + .../downloads/versions/VersionRoutes.java | 72 ++++ .../versions/models/JpaTaggedVersion.java | 170 +++++++++ .../models/JpaVersionedArtifactView.java | 231 +++++++++++++ .../versions/models/JpaVersionedAsset.java | 146 ++++++++ .../models/JpaVersionedChangelog.java | 140 ++++++++ .../versions/models/VersionedArtifactID.java | 59 ++++ .../versions/models/VersionedAssetID.java | 12 + .../versions/transport/QueryLatest.java | 46 +++ .../versions/transport/QueryVersions.java | 57 +++ .../versions/transport/TagCollection.java | 35 ++ .../transport/VersionedChangelog.java | 65 ++++ .../versions/transport/VersionedCommit.java | 69 ++++ .../src/main/resources/application.conf | 6 + downloads-api/src/main/resources/logback.xml | 20 ++ .../test/java/com/example/UserRoutesTest.java | 77 +++++ .../src/test/resources/application-test.conf | 3 + .../gitmanaged/domain/GitEvent.java | 6 +- .../src/main/resources/logback.xml | 4 +- .../worker/CommitResolutionManagerTest.java | 17 + 49 files changed, 3270 insertions(+), 69 deletions(-) create mode 100644 docs/Dockerfile create mode 100644 docs/project/build.properties create mode 100644 docs/systemofadownload.yaml create mode 100644 downloads-api/src/main/java/com/example/QuickstartApp.java create mode 100644 downloads-api/src/main/java/com/example/UserRegistry.java create mode 100644 downloads-api/src/main/java/com/example/UserRoutes.java create mode 100644 downloads-api/src/main/java/module-info.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java create mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java create mode 100644 downloads-api/src/main/resources/application.conf create mode 100644 downloads-api/src/main/resources/logback.xml create mode 100644 downloads-api/src/test/java/com/example/UserRoutesTest.java create mode 100644 downloads-api/src/test/resources/application-test.conf diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java index 81e534ae..b9d21ab4 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java @@ -30,67 +30,19 @@ import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.Group; -import java.util.Objects; - public final class GroupRegistration { @JsonDeserialize - public static final class RegisterGroupRequest { - - /** - * The name of the group, displayed for reading purposes - */ - @JsonProperty(required = true) - public final String name; - /** - * The maven group coordinates of the group. - */ - @JsonProperty(required = true) - public final String groupCoordinates; - /** - * A website for the group - */ - @JsonProperty(required = true) - public final String website; - - @JsonCreator - public RegisterGroupRequest( - final String name, - final String groupCoordinates, - final String website - ) { - this.name = name; - this.groupCoordinates = groupCoordinates; - this.website = website; - } + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website + ) { - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (RegisterGroupRequest) obj; - return Objects.equals(this.name, that.name) && - Objects.equals(this.groupCoordinates, that.groupCoordinates) && - Objects.equals(this.website, that.website); - } - - @Override - public int hashCode() { - return Objects.hash(this.name, this.groupCoordinates, this.website); - } + @JsonCreator + public RegisterGroupRequest { } - @Override - public String toString() { - return "RegisterGroupRequest[" + - "name=" + this.name + ", " + - "groupCoordinates=" + this.groupCoordinates + ", " + - "website=" + this.website + ']'; } - } public interface Response extends Jsonable { diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java index 1332d045..78416658 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java +++ b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java @@ -45,5 +45,4 @@ record Available(@JsonProperty List groups) public Available { } } - } diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java index e7eb24e2..573fde7f 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/group/GroupManager.java @@ -37,10 +37,10 @@ public GroupManager(ClusterSharding clusterSharding, final DetailsManager detail public CompletionStage registerGroup( GroupRegistration.RegisterGroupRequest registration ) { - final String mavenCoordinates = registration.groupCoordinates; - final String name = registration.name; - final String website = registration.website; - return this.groupEntity(registration.groupCoordinates.toLowerCase(Locale.ROOT)) + final String mavenCoordinates = registration.groupCoordinates(); + final String name = registration.name(); + final String website = registration.website(); + return this.groupEntity(registration.groupCoordinates().toLowerCase(Locale.ROOT)) .ask( replyTo -> new GroupCommand.RegisterGroup(mavenCoordinates, name, website, replyTo), this.askTimeout diff --git a/build.sbt b/build.sbt index a9bfa109..0260d511 100644 --- a/build.sbt +++ b/build.sbt @@ -16,10 +16,10 @@ ThisBuild / scmInfo := Some(ScmInfo(url("https://github.com/SpongePowered/System "scm:git@github.com:spongepowered/systemofadownload.git")) ThisBuild / developers := List( Developer( - id = "gabizou", - name = "Gabriel Harris-Rouquette", + id = "gabizou", + name = "Gabriel Harris-Rouquette", email = "gabizou@spongepowered.org", - url = url("https://github.com/gabizou") + url = url("https://github.com/gabizou") ) ) ThisBuild / description := "A Web Application for indexing and cataloging Artifacts in Maven Repositories" @@ -127,6 +127,9 @@ lazy val jacksonGuava = "com.fasterxml.jackson.datatype" % "jackson-datatype-gua lazy val jacksonPcollections = "com.fasterxml.jackson.datatype" % "jackson-datatype-pcollections" % "2.14.0" // endregion +lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % LagomVersion.akkaHttp +lazy val akkaJackson = "com.typesafe.akka" %% "akka-http-jackson" % LagomVersion.akkaHttp + lazy val akkaStreamTyped = "com.typesafe.akka" %% "akka-stream-typed" % LagomVersion.akka lazy val akkaPersistenceTestkit = "com.typesafe.akka" %% "akka-persistence-testkit" % LagomVersion.akka % Test lazy val akkaKubernetesDiscovery = "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % "1.1.3" @@ -142,6 +145,12 @@ lazy val guice = "com.google.inject" % "guice" % "5.1.0" lazy val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "6.1.0.202203080745-r" lazy val jgit_jsch = "org.eclipse.jgit" % "org.eclipse.jgit.ssh.jsch" % "6.1.0.202203080745-r" +lazy val mavenArtifact = "org.apache.maven" % "maven-artifact" % "3.8.5" + +lazy val testContainers = "org.testcontainers" % "testcontainers" % "1.17.3" % Test +lazy val testContainersJunit = "org.testcontainers" % "junit-jupiter" % "1.17.3" % Test +lazy val testContainersPostgres = "org.testcontainers" % "postgresql" % "1.17.3" % Test + // endregion // region - project blueprints @@ -149,7 +158,7 @@ lazy val jgit_jsch = "org.eclipse.jgit" % "org.eclipse.jgit.ssh.jsch" % "6.1.0.2 def soadProject(name: String) = Project(name, file(name)).settings( moduleName := s"systemofadownload-$name", - Compile / javacOptions := Seq("--release", "17", "-parameters", "-encoding", "UTF-8"), //Override the settings Lagom sets + Compile / javacOptions := Seq("--release", "17", "--enable-preview", "-parameters", "-encoding", "UTF-8"), //Override the settings Lagom sets artifactName := { (_: ScalaVersion, module: ModuleID, artifact: Artifact) => s"${artifact.name}-${module.revision}.${artifact.extension}" }, @@ -308,6 +317,50 @@ lazy val `artifact-query-api` = apiSoadProject("artifact-query-api").dependsOn( lazy val `artifact-query-impl` = implSoadProjectWithPersistence("artifact-query-impl", `artifact-query-api`).settings( libraryDependencies += playFilterHelpers ) +lazy val `downloads-api` = soadProject("downloads-api").enablePlugins(DockerPlugin) + .settings( + libraryDependencies ++= Seq( + // App + mavenArtifact, + + // Akka + akkaHttp, + akkaStreamTyped, + akkaKubernetesDiscovery, + + // Jackson serialization + akkaJackson, + jacksonDataBind, + jacksonDataTypeJsr310, + jacksonDataformatCbor, + jacksonDatatypeJdk8, + jacksonParameterNames, + jacksonParanamer, + jacksonScala, + //Language Features + vavr, + // Persistence + hibernate, + postgres, + // Testing + akkaPersistenceTestkit, + junit, + jupiterInterface + ), + dockerUpdateLatest := true, + dockerBaseImage := "eclipse-temurin:17.0.3_7-jre", + dockerChmodType := DockerChmodType.UserGroupWriteExecute, + dockerExposedPorts := Seq(9000, 8558, 2552), + // dockerLabels ++= Map( + // "author" -> "spongepowered" + // ), + Docker / maintainer := "spongepowered", + Docker / packageName := s"systemofadownload-$name", + dockerUsername := Some("spongepowered"), + Universal / javaOptions ++= Seq( + "-Dpidfile.path=/dev/null" + ) + ) lazy val `versions-api` = apiSoadProject("versions-api").dependsOn( //Module Dependencies diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..a7bb68a4 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,8 @@ +FROM node:18-alpine as builder + +COPY systemofadownload.yaml ./ +RUN npm install -g redoc-cli && redoc-cli bundle -o index.html systemofadownload.yaml + +FROM nginx as webserver + +COPY --from=builder index.html /usr/share/nginx/html/ diff --git a/docs/project/build.properties b/docs/project/build.properties new file mode 100644 index 00000000..1e70b0c1 --- /dev/null +++ b/docs/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.6.0 diff --git a/docs/systemofadownload.yaml b/docs/systemofadownload.yaml new file mode 100644 index 00000000..2cc30669 --- /dev/null +++ b/docs/systemofadownload.yaml @@ -0,0 +1,324 @@ +openapi: 3.0.1 +info: + title: SystemOfADownload - API + description: >- + An indexing service for making downloads of maven artifacts easier and more + human readable. + version: 1.0.0 +servers: + - url: https://dl-api-new.spongepowered.org/api/v2 + description: The main API server +paths: + /groups: + get: + summary: Get list of registered groups + operationId: getGroups + responses: + '200': + description: The list of groups registered + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Group' + /groups/{groupID}: + get: + summary: Get a group information + operationId: getGroup + parameters: + - $ref: '#/components/parameters/GroupID' + responses: + '200': + $ref: '#/components/responses/GroupResponse' + '404': + $ref: '#/components/responses/404' + /groups/{groupID}/artifacts: + get: + operationId: getArtifacts + parameters: + - $ref: '#/components/parameters/GroupID' + responses: + '200': + $ref: '#/components/responses/200ArtifactsAvailable' + '404': + $ref: '#/components/responses/404' + /groups/{groupID}/artifacts/{artifactID}: + get: + operationId: getArtifact + parameters: + - $ref: '#/components/parameters/GroupID' + - $ref: '#/components/parameters/ArtifactId' + responses: + '200': + description: The artifact information + content: + application/json: + schema: + $ref: '#/components/schemas/Artifact' + '404': + $ref: '#/components/responses/404' + /groups/{groupID}/artifacts/{artifactID}/versions: + get: + operationId: getVersions + parameters: + - $ref: '#/components/parameters/GroupID' + - $ref: '#/components/parameters/ArtifactId' + - $ref: '#/components/parameters/Tags' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Offset' + responses: + '200': + description: The list of versions available + content: + application/json: + schema: + properties: + artifacts: + type: object + additionalProperties: + $ref: '#/components/schemas/Version' + example: + 1.12.2-7.4.7: + tagValues: + minecraft: 1.12.2 + api: '7.4' + recommended: true + '404': + $ref: '#/components/responses/404' + /groups/{groupID}/artifacts/{artifactID}/versions/{version}: + summary: versionDetails + get: + operationId: getVersionDetails + parameters: + - $ref: '#/components/parameters/GroupID' + - $ref: '#/components/parameters/ArtifactId' + - $ref: '#/components/parameters/Version' + responses: + '200': + description: The details of the version specifically + content: + application/json: + schema: + properties: + +components: + schemas: + Group: + type: object + properties: + groupCoordinates: + type: string + description: The maven coordinates for the group, e.g. org.spongepowered + name: + type: string + description: >- + The name of the group, often times different than the group + coordinates, e.g. SpongePowered + website: + type: string + description: >- + The registered website for the group, usually a homepage, e.g. + https://www.spongepowered.org + GroupMissing: + type: object + properties: + groupID: + type: string + description: The group ID in maven coordinate format, e.g. com.example + Coordinates: + type: object + properties: + groupId: + type: string + description: The group ID in maven coordinate format, e.g. com.example + example: org.spongepowered + artifactId: + type: string + description: The artifact ID in maven coordinate format, e.g. example-plugin + example: spongevanilla + Artifact: + description: An artifact with information + type: object + properties: + coordinates: + $ref: '#/components/schemas/Coordinates' + name: + type: string + description: The name of the artifact + example: spongevanilla + displayName: + type: string + description: The display name of the artifact + example: SpongeVanilla + website: + type: string + description: The website of the artifact + example: https://spongepowered.org/ + issues: + type: string + description: The url for submitting issues + example: https://github.com/SpongePowered/SpongeVanilla/issues + gitRepository: + type: string + description: The git repository of the artifact + example: https://github.com/spongepowered/sponge + tags: + type: object + example: + api: + - '9.0' + - '8.1' + - '8.0' + - '7.4' + - '7.3' + - '7.2' + - '7.1' + - '7.0' + - '5.0' + minecraft: + - 1.18.2 + - 1.16.5 + - 1.12.2 + - 1.12.1 + - '1.12' + - 1.11.2 + - '1.11' + - 1.10.2 + - 1.9.4 + - '1.9' + - 1.8.9 + - '1.8' + additionalProperties: + type: array + items: + type: string + example: + - 1.16.5 + - 1.12.2 + - '1.12' + - 1.10.2 + - 1.8.9 + - '1.8' + Version: + type: object + properties: + recommended: + type: boolean + description: Whether or not this version is recommended + example: true + tagValues: + type: object + description: The tag values for this version + additionalProperties: + type: string + description: The value of the tag + example: + api: '8.0' + minecraft: 1.16.5 + parameters: + GroupID: + name: groupID + in: path + description: The group ID in maven coordinate format + example: org.spongepowered + required: true + schema: + type: string + ArtifactId: + name: artifactID + in: path + description: The artifact id in maven coordinate format + example: spongevanilla + required: true + schema: + type: string + Recommended: + name: recommended + in: query + description: Whether to only return recommended versions + example: true + schema: + type: boolean + Tags: + name: tags + in: query + schema: + type: string + description: > + The tags to filter by. Formatted in a comma separated list of tags where + the tag key + + and value are joined by a colon. Chaining multiple tags is supported. + Conflicting tags + + will be ignored. + example: api:8.0,minecraft:1.16.5 + Limit: + name: limit + in: query + description: The limit of results to return + example: 10 + schema: + type: integer + minimum: 1 + maximum: 10 + Offset: + name: offset + in: query + description: The offset of results to return + example: 10 + schema: + type: integer + Version: + name: version + in: path + description: The version string + example: '1.16.5-8.1.0-RC1153' + required: true + schema: + type: string + responses: + '404': + description: Not found + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: The name of the error + example: NotFound + detail: + type: string + description: The detail of the error + example: group or artifact not found + GroupResponse: + description: Available Group Information + content: + application/json: + schema: + type: object + properties: + group: + $ref: '#/components/schemas/Group' + 404GroupMissing: + description: Group not found + content: + application/json: + schema: + $ref: '#/components/schemas/GroupMissing' + 200ArtifactsAvailable: + description: Artifacts available within a Group + content: + application/json: + schema: + type: object + properties: + artifactIds: + type: array + items: + type: string + description: >- + The maven coordinate formatted id of an artifact, e.g. + spongevanilla diff --git a/downloads-api/src/main/java/com/example/QuickstartApp.java b/downloads-api/src/main/java/com/example/QuickstartApp.java new file mode 100644 index 00000000..ac1187ee --- /dev/null +++ b/downloads-api/src/main/java/com/example/QuickstartApp.java @@ -0,0 +1,62 @@ +package com.example; + +import akka.NotUsed; +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.http.javadsl.ConnectHttp; +import akka.http.javadsl.Http; +import akka.http.javadsl.ServerBinding; +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.HttpResponse; +import akka.http.javadsl.server.Route; +import akka.stream.Materializer; +import akka.stream.javadsl.Flow; +import akka.actor.typed.javadsl.Adapter; +import akka.actor.typed.javadsl.Behaviors; +import akka.actor.typed.ActorSystem; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletionStage; + +//#main-class +public class QuickstartApp { + // #start-http-server + static void startHttpServer(Route route, ActorSystem system) { + CompletionStage futureBinding = + Http.get(system).newServerAt("localhost", 8080).bind(route); + + futureBinding.whenComplete((binding, exception) -> { + if (binding != null) { + InetSocketAddress address = binding.localAddress(); + system.log().info("Server online at http://{}:{}/", + address.getHostString(), + address.getPort()); + } else { + system.log().error("Failed to bind HTTP endpoint, terminating system", exception); + system.terminate(); + } + }); + } + // #start-http-server + + public static void main(String[] args) throws Exception { + //#server-bootstrapping + Behavior rootBehavior = Behaviors.setup(context -> { + ActorRef userRegistryActor = + context.spawn(UserRegistry.create(), "UserRegistry"); + + UserRoutes userRoutes = new UserRoutes(context.getSystem(), userRegistryActor); + startHttpServer(userRoutes.userRoutes(), context.getSystem()); + + return Behaviors.empty(); + }); + + // boot up server using the route as defined below + ActorSystem.create(rootBehavior, "HelloAkkaHttpServer"); + //#server-bootstrapping + } + +} +//#main-class + + diff --git a/downloads-api/src/main/java/com/example/UserRegistry.java b/downloads-api/src/main/java/com/example/UserRegistry.java new file mode 100644 index 00000000..c8d914d2 --- /dev/null +++ b/downloads-api/src/main/java/com/example/UserRegistry.java @@ -0,0 +1,101 @@ +package com.example; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.AbstractBehavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +import akka.actor.typed.javadsl.Receive; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.*; + +//#user-registry-actor +public class UserRegistry extends AbstractBehavior { + + // actor protocol + interface Command {} + + public record GetUsers(ActorRef replyTo) implements Command { + } + + public record CreateUser(User user, ActorRef replyTo) implements Command { + } + + public record GetUserResponse(Optional maybeUser) { + } + + public record GetUser(String name, ActorRef replyTo) implements Command { + } + + + public record DeleteUser(String name, ActorRef replyTo) implements Command { + } + + + public record ActionPerformed(String description) implements Command { + } + + //#user-case-classes + public record User( + @JsonProperty("name") String name, + @JsonProperty("age") int age, + @JsonProperty("countryOfResidence") String countryOfResidence + ) { + @JsonCreator + public User {} + } + + public record Users(List users) { } + //#user-case-classes + + private final List users = new ArrayList<>(); + + private UserRegistry(ActorContext context) { + super(context); + } + + public static Behavior create() { + return Behaviors.setup(UserRegistry::new); + } + + @Override + public Receive createReceive() { + return newReceiveBuilder() + .onMessage(GetUsers.class, this::onGetUsers) + .onMessage(CreateUser.class, this::onCreateUser) + .onMessage(GetUser.class, this::onGetUser) + .onMessage(DeleteUser.class, this::onDeleteUser) + .build(); + } + + private Behavior onGetUsers(GetUsers command) { + // We must be careful not to send out users since it is mutable + // so for this response we need to make a defensive copy + command.replyTo.tell(new Users(List.copyOf(users))); + return this; + } + + private Behavior onCreateUser(CreateUser command) { + users.add(command.user); + command.replyTo.tell(new ActionPerformed(String.format("User %s created.", command.user.name))); + return this; + } + + private Behavior onGetUser(GetUser command) { + Optional maybeUser = users.stream() + .filter(user -> user.name.equals(command.name)) + .findFirst(); + command.replyTo.tell(new GetUserResponse(maybeUser)); + return this; + } + + private Behavior onDeleteUser(DeleteUser command) { + users.removeIf(user -> user.name.equals(command.name)); + command.replyTo.tell(new ActionPerformed(String.format("User %s deleted.", command.name))); + return this; + } + +} +//#user-registry-actor diff --git a/downloads-api/src/main/java/com/example/UserRoutes.java b/downloads-api/src/main/java/com/example/UserRoutes.java new file mode 100644 index 00000000..6eea10f3 --- /dev/null +++ b/downloads-api/src/main/java/com/example/UserRoutes.java @@ -0,0 +1,112 @@ +package com.example; + +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +import com.example.UserRegistry.User; +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Scheduler; +import akka.actor.typed.javadsl.AskPattern; +import akka.http.javadsl.marshallers.jackson.Jackson; + +import static akka.http.javadsl.server.Directives.*; + +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.server.PathMatchers; +import akka.http.javadsl.server.Route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Routes can be defined in separated classes like shown in here + */ +//#user-routes-class +public class UserRoutes { + //#user-routes-class + private final static Logger log = LoggerFactory.getLogger(UserRoutes.class); + private final ActorRef userRegistryActor; + private final Duration askTimeout; + private final Scheduler scheduler; + + public UserRoutes(ActorSystem system, ActorRef userRegistryActor) { + this.userRegistryActor = userRegistryActor; + scheduler = system.scheduler(); + askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); + } + + private CompletionStage getUser(String name) { + return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.GetUser(name, ref), askTimeout, scheduler); + } + + private CompletionStage deleteUser(String name) { + return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.DeleteUser(name, ref), askTimeout, scheduler); + } + + private CompletionStage getUsers() { + return AskPattern.ask(userRegistryActor, UserRegistry.GetUsers::new, askTimeout, scheduler); + } + + private CompletionStage createUser(User user) { + return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.CreateUser(user, ref), askTimeout, scheduler); + } + + /** + * This method creates one route (of possibly many more that will be part of your Web App) + */ + //#all-routes + public Route userRoutes() { + return pathPrefix("users", () -> + concat( + //#users-get-delete + pathEnd(() -> + concat( + get(() -> + onSuccess(getUsers(), + users -> complete(StatusCodes.OK, users, Jackson.marshaller()) + ) + ), + post(() -> + entity( + Jackson.unmarshaller(User.class), + user -> + onSuccess(createUser(user), performed -> { + log.info("Create result: {}", performed.description()); + return complete(StatusCodes.CREATED, performed, Jackson.marshaller()); + }) + ) + ) + ) + ), + //#users-get-delete + //#users-get-post + path(PathMatchers.segment(), (String name) -> + concat( + get(() -> + //#retrieve-user-info + rejectEmptyResponse(() -> + onSuccess(getUser(name), performed -> + complete(StatusCodes.OK, performed.maybeUser(), Jackson.marshaller()) + ) + ) + //#retrieve-user-info + ), + delete(() -> + //#users-delete-logic + onSuccess(deleteUser(name), performed -> { + log.info("Delete result: {}", performed.description()); + return complete(StatusCodes.OK, performed, Jackson.marshaller()); + } + ) + //#users-delete-logic + ) + ) + ) + //#users-get-post + ) + ); + } + //#all-routes + +} diff --git a/downloads-api/src/main/java/module-info.java b/downloads-api/src/main/java/module-info.java new file mode 100644 index 00000000..e4c4aee5 --- /dev/null +++ b/downloads-api/src/main/java/module-info.java @@ -0,0 +1,17 @@ +module org.spongepowered.downloads.app { + exports org.spongepowered.downloads.api; + + requires akka.actor.typed; + requires akka.http; + requires akka.http.core; + requires akka.stream; + requires akka.actor; + requires com.fasterxml.jackson.annotation; + requires akka.http.marshallers.jackson; + requires org.slf4j; + requires org.hibernate.orm.core; + requires java.persistence; + requires io.vavr; + requires com.fasterxml.jackson.databind; + requires maven.artifact; +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java new file mode 100644 index 00000000..a1a4cb4c --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java @@ -0,0 +1,46 @@ +/* + * 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.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.net.URI; +import java.util.Optional; + +@JsonSerialize +public final record Artifact( + @JsonProperty Optional classifier, + @JsonProperty URI downloadUrl, + @JsonProperty String md5, + @JsonProperty String sha1, + @JsonProperty String extension +) { + @JsonCreator + public Artifact { + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java new file mode 100644 index 00000000..a3b29fe8 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java @@ -0,0 +1,42 @@ +/* + * 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.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.collection.List; + +@JsonDeserialize +public final record ArtifactCollection( + @JsonProperty("assets") List components, + @JsonProperty("coordinates") MavenCoordinates coordinates +) { + + @JsonCreator + public ArtifactCollection { + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java new file mode 100644 index 00000000..b9f11573 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java @@ -0,0 +1,66 @@ +/* + * 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.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.StringJoiner; + +@JsonDeserialize +public record ArtifactCoordinates( + @JsonProperty(required = true) String groupId, + @JsonProperty(required = true) String artifactId +) { + @JsonCreator + public ArtifactCoordinates { + } + + public MavenCoordinates version(String version) { + return MavenCoordinates.parse( + new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); + } + + public String asMavenString() { + return this.groupId() + ":" + this.artifactId(); + } + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String groupId() { + return groupId; + } + + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String artifactId() { + return artifactId; + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java new file mode 100644 index 00000000..4efe1286 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java @@ -0,0 +1,42 @@ +/* + * 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.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public record Group( + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String website +) { + + @JsonCreator + public Group { + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java new file mode 100644 index 00000000..cab9ae7a --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java @@ -0,0 +1,192 @@ +/* + * 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.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +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; +import java.util.regex.Pattern; + +@JsonDeserialize +public final class MavenCoordinates implements Comparable { + + private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String groupId; + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String artifactId; + /** + * The version of an artifact, as defined by the Apache Maven documentation. This is + * traditionally specified as a Maven repository searchable version string, such as + * {@code 1.0.0-SNAPSHOT}. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String version; + + @JsonIgnore + public final VersionType versionType; + + @JsonIgnore + private final ComparableVersion mavenVersion; + + /** + * Parses a set of maven formatted coordinates as per + * Apache + * Maven's documentation. + * + * @param coordinates The coordinates delimited by `:` + * @return A parsed set of MavenCoordinates + */ + public static MavenCoordinates parse(final String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + return new MavenCoordinates(groupId, artifactId, version); + } + + public MavenCoordinates(String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonCreator + public MavenCoordinates( + @JsonProperty("groupId") final String groupId, + @JsonProperty("artifactId") final String artifactId, + @JsonProperty("version") final String version + ) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonIgnore + public String asStandardCoordinates() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.versionType.asStandardVersionString(this.version)) + .toString(); + } + + @JsonIgnore + public boolean isSnapshot() { + return this.versionType.isSnapshot(); + } + + @JsonIgnore + public ArtifactCoordinates asArtifactCoordinates() { + return new ArtifactCoordinates(this.groupId, this.artifactId); + } + + @Override + public String toString() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.version) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final MavenCoordinates that = (MavenCoordinates) o; + return Objects.equals(this.groupId, that.groupId) && Objects.equals( + this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(this.groupId, this.artifactId, this.version); + } + + @Override + public int compareTo(final MavenCoordinates o) { + final var group = this.groupId.compareTo(o.groupId); + if (group != 0) { + return group; + } + final var artifact = this.artifactId.compareTo(o.artifactId); + if (artifact != 0) { + return artifact; + } + return this.mavenVersion.compareTo(o.mavenVersion); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java b/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java new file mode 100644 index 00000000..f5e37946 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java @@ -0,0 +1,52 @@ +package org.spongepowered.downloads.app; + +import akka.NotUsed; +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.Behaviors; +import akka.http.javadsl.Http; +import akka.http.javadsl.ServerBinding; +import akka.http.javadsl.server.Route; +import org.spongepowered.downloads.artifacts.ArtifactQueries; +import org.spongepowered.downloads.artifacts.ArtifactRoutes; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletionStage; + +public final class SystemOfADownloadsApp { + + public static void main(final String[] args) { + //#server-bootstrapping + Behavior rootBehavior = Behaviors.setup(context -> { + ActorRef userRegistryActor = + context.spawn(ArtifactQueries.create(), "ArtifactQueries"); + + ArtifactRoutes userRoutes = new ArtifactRoutes(context.getSystem(), userRegistryActor); + startHttpServer(userRoutes.artifactRoutes(), context.getSystem()); + + return Behaviors.empty(); + }); + + // boot up server using the route as defined below + ActorSystem.create(rootBehavior, "HelloAkkaHttpServer"); + //#server-bootstrapping + } + + static void startHttpServer(Route route, ActorSystem system) { + CompletionStage futureBinding = + Http.get(system).newServerAt("localhost", 8080).bind(route); + + futureBinding.whenComplete((binding, exception) -> { + if (binding != null) { + InetSocketAddress address = binding.localAddress(); + system.log().info("Server online at http://{}:{}/", + address.getHostString(), + address.getPort()); + } else { + system.log().error("Failed to bind HTTP endpoint, terminating system", exception); + system.terminate(); + } + }); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java new file mode 100644 index 00000000..8aa8ad01 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java @@ -0,0 +1,52 @@ +package org.spongepowered.downloads.artifacts; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.javadsl.AbstractBehavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Receive; +import org.spongepowered.downloads.artifacts.transport.GetArtifactDetailsResponse; +import org.spongepowered.downloads.artifacts.transport.GetArtifactsResponse; +import org.spongepowered.downloads.artifacts.transport.GroupResponse; +import org.spongepowered.downloads.artifacts.transport.GroupsResponse; + +class ArtifactQueries extends AbstractBehavior { + + public ArtifactQueries(final ActorContext context) { + super(context); + } + + sealed interface Command { + + record GetGroup( + String groupId, + ActorRef replyTo + ) implements Command { + } + + record GetArtifacts( + String groupId, + ActorRef replyTo + ) implements Command { + } + + record GetGroups( + ActorRef replyTo + ) implements Command { + } + + record GetArtifactDetails( + String groupId, + String artifactId, + ActorRef replyTo + ) implements Command { + } + + } + + + @Override + public Receive createReceive() { + return this.newReceiveBuilder() + .build(); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java new file mode 100644 index 00000000..513f7213 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java @@ -0,0 +1,89 @@ +package org.spongepowered.downloads.artifacts; + +import static akka.http.javadsl.server.Directives.complete; +import static akka.http.javadsl.server.Directives.concat; +import static akka.http.javadsl.server.Directives.get; +import static akka.http.javadsl.server.Directives.onSuccess; +import static akka.http.javadsl.server.Directives.path; +import static akka.http.javadsl.server.Directives.pathPrefix; +import static akka.http.javadsl.server.Directives.rejectEmptyResponse; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Scheduler; +import akka.actor.typed.javadsl.AskPattern; +import akka.http.javadsl.marshallers.jackson.Jackson; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.server.PathMatchers; +import akka.http.javadsl.server.Route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.downloads.artifacts.transport.GetArtifactsResponse; +import org.spongepowered.downloads.artifacts.transport.GroupResponse; +import org.spongepowered.downloads.artifacts.transport.GroupsResponse; + +import java.time.Duration; +import java.util.concurrent.CompletionStage; + +public final class ArtifactRoutes { + + public static final Logger logger = LoggerFactory.getLogger(ArtifactRoutes.class); + + private final ActorRef artifactQueries; + private final Duration askTimeout; + private final Scheduler scheduler; + + public ArtifactRoutes(ActorSystem system, ActorRef artifactQueries) { + this.artifactQueries = artifactQueries; + scheduler = system.scheduler(); + askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); + } + + private CompletionStage getGroups() { + return AskPattern.ask(artifactQueries, ArtifactQueries.Command.GetGroups::new, askTimeout, scheduler); + } + + private CompletionStage getGroup(String name) { + return AskPattern.ask( + artifactQueries, ref -> new ArtifactQueries.Command.GetGroup(name, ref), askTimeout, scheduler); + } + + private CompletionStage getArtifacts(String name) { + return AskPattern.ask( + artifactQueries, ref -> new ArtifactQueries.Command.GetArtifacts(name, ref), askTimeout, scheduler); + } + + + /** + * This method creates one route (of possibly many more that will be part of your Web App) + */ + public Route artifactRoutes() { + // v1/groups + return pathPrefix("groups", () -> + concat( + get(() -> onSuccess(getGroups(), groups -> + complete(StatusCodes.OK, groups, Jackson.marshaller()) + )), + // v1/groups/:groupId/ + path(PathMatchers.segment(), (String groupId) -> + concat( + get(() -> rejectEmptyResponse(() -> + onSuccess(getGroup(groupId), performed -> { + final var group = performed.group(); + logger.info("Groups get: {}", group); + if (group.isEmpty()) { + return complete(StatusCodes.BAD_REQUEST, "group not found"); + } + return complete(StatusCodes.OK, group, Jackson.marshaller()); + }))), + // v1/groups/:groupId/artifacts + pathPrefix("artifacts", () -> get(() -> rejectEmptyResponse(() -> + onSuccess(getArtifacts(groupId), performed -> { + logger.info("Get result: {}", performed.artifactIds()); + return complete(StatusCodes.OK, performed, Jackson.marshaller()); + }))) + )) + ) + )); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java new file mode 100644 index 00000000..0ee0309d --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java @@ -0,0 +1,181 @@ +/* + * 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.models; + + +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.Map; +import io.vavr.collection.SortedSet; +import io.vavr.collection.TreeMap; +import io.vavr.collection.TreeSet; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.hibernate.annotations.Immutable; +import org.spongepowered.downloads.api.ArtifactCoordinates; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.io.Serializable; +import java.util.Comparator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Immutable +@Entity(name = "Artifact") +@Table(name = "artifacts", + schema = "version") +@NamedQueries({ + @NamedQuery( + name = "Artifact.findByCoordinates", + query = "select a from Artifact a where a.groupId = :groupId and a.artifactId = :artifactId" + ) +}) +public class JpaArtifact implements Serializable { + + @Id + @Column(name = "id", + nullable = false, + updatable = false, + insertable = false) + private int id; + + @Column(name = "group_id", + nullable = false, + updatable = false, + insertable = false) + private String groupId; + + @Column(name = "artifact_id", + nullable = false, + updatable = false, + insertable = false) + private String artifactId; + + @Column(name = "display_name", + updatable = false, + insertable = false) + private String displayName; + + @Column(name = "website", + updatable = false, + insertable = false) + private String website; + + @Column(name = "git_repository", + updatable = false, + insertable = false) + private String gitRepo; + + @Column(name = "issues", + updatable = false, + insertable = false) + private String issues; + + @OneToMany(fetch = FetchType.EAGER, + targetEntity = JpaArtifactTagValue.class) + @JoinColumns({ + @JoinColumn(name = "artifact_id", + referencedColumnName = "artifact_id"), + @JoinColumn(name = "group_id", + referencedColumnName = "group_id") + }) + private Set tagValues; + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getDisplayName() { + return displayName; + } + + public String getWebsite() { + return website; + } + + public String getGitRepo() { + return gitRepo; + } + + public String getIssues() { + return issues; + } + + public Set getTagValues() { + return tagValues; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaArtifact that = (JpaArtifact) o; + return id == that.id && groupId.equals(that.groupId) && artifactId.equals( + that.artifactId) && Objects.equals(displayName, that.displayName) && Objects.equals( + website, that.website) && Objects.equals(gitRepo, that.gitRepo) && Objects.equals( + issues, that.issues); + } + + @Override + public int hashCode() { + return Objects.hash(id, groupId, artifactId, displayName, website, gitRepo, issues); + } + + public ArtifactCoordinates getCoordinates() { + return new ArtifactCoordinates(this.groupId, this.artifactId); + } + + public Map> getTagValuesForReply() { + final var tagValues = this.getTagValues(); + final var tagTuples = tagValues.stream() + .map(value -> Tuple.of(value.getTagName(), value.getTagValue())) + .toList(); + + var versionedTags = TreeMap.>empty(); + final var comparator = Comparator.comparing(ComparableVersion::new).reversed(); + + for (final Tuple2 tagged : tagTuples) { + versionedTags = versionedTags.put(tagged._1, TreeSet.of(comparator, tagged._2), SortedSet::addAll); + } + return versionedTags; + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java new file mode 100644 index 00000000..d6a92e3d --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java @@ -0,0 +1,137 @@ +/* + * 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.models; + +import org.hibernate.annotations.Immutable; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import java.io.Serializable; +import java.util.Objects; + +@Immutable +@Entity(name = "ArtifactTagValue") +@Table(name = "artifact_tag_values", + schema = "version") +@IdClass(JpaArtifactTagValue.Identifier.class) +public class JpaArtifactTagValue { + + /* + This identifier is required to list basically the columns all available + because JPA will consider the specific id's as "unique", but since this is + not a unique columnar list, we treat all the columns as '@Id'. In this way, + we're able to gather all the unique tag values from the artifact by the + abused JoinColumns. + */ + static final class Identifier implements Serializable { + String artifactId; + String groupId; + String tagName; + String tagValue; + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Identifier that = (Identifier) o; + return Objects.equals(artifactId, that.artifactId) && Objects.equals( + groupId, that.groupId) && Objects.equals(tagName, that.tagName) && Objects.equals( + tagValue, that.tagValue); + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, groupId, tagName, tagValue); + } + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "artifact_id", + referencedColumnName = "artifact_id"), + @JoinColumn(name = "group_id", + referencedColumnName = "group_id") + }) + private JpaArtifact artifact; + + @Id + @Column(name = "artifact_id", + insertable = false, + updatable = false) + private String artifactId; + @Id + @Column(name = "group_id", + insertable = false, + updatable = false) + private String groupId; + + @Id + @Column(name = "tag_name", + insertable = false, + updatable = false) + private String tagName; + + @Id + @Column(name = "tag_value", + insertable = false, + updatable = false) + private String tagValue; + + public String getTagName() { + return tagName; + } + + public String getTagValue() { + return tagValue; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaArtifactTagValue that = (JpaArtifactTagValue) o; + return artifact.equals(that.artifact) && tagName.equals(that.tagName) && tagValue.equals(that.tagValue); + } + + @Override + public int hashCode() { + return Objects.hash(artifact, tagName, tagValue); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java new file mode 100644 index 00000000..7fad1598 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java @@ -0,0 +1,128 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.control.Either; +import io.vavr.control.Try; + +import java.net.URL; + +public final class ArtifactDetails { + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Update.Website.class, + name = "website"), + @JsonSubTypes.Type(value = Update.DisplayName.class, + name = "displayName"), + @JsonSubTypes.Type(value = Update.Issues.class, + name = "issues"), + @JsonSubTypes.Type(value = Update.GitRepository.class, + name = "gitRepository"), + }) + @JsonDeserialize + public sealed interface Update { + + Either validate(); + + record Website( + @JsonProperty(required = true) String website + ) implements Update { + + @JsonCreator + public Website { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.website())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.website()))); + } + } + + record DisplayName( + @JsonProperty(required = true) String display + ) implements Update { + + @JsonCreator + public DisplayName { + } + + @Override + public Either validate() { + return Either.right(this.display.trim()); + } + } + + record Issues( + @JsonProperty(required = true) String issues + ) implements Update { + @JsonCreator + public Issues { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.issues())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.issues()))); + } + } + + record GitRepository( + @JsonProperty(required = true) String gitRepo + ) implements Update { + + @JsonCreator + public GitRepository { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.gitRepo())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.gitRepo()))); + } + } + } + + @JsonSerialize + public record Response( + String name, + String displayName, + String website, + String issues, + String gitRepo + ) { + + } + + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java new file mode 100644 index 00000000..4ab6dcc7 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java @@ -0,0 +1,82 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +public final class ArtifactRegistration { + + @JsonSerialize + public record RegisterArtifact( + @JsonProperty(required = true) String artifactId, + @JsonProperty(required = true) String displayName + ) { + + @JsonCreator + public RegisterArtifact(final String artifactId, final String displayName) { + this.artifactId = artifactId; + this.displayName = displayName; + } + + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Response.GroupMissing.class, + name = "UnknownGroup"), + @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, + name = "RegisteredArtifact"), + @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, + name = "AlreadyRegistered"), + }) + public sealed interface Response extends Jsonable { + + @JsonSerialize + record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { + + } + + @JsonSerialize + record ArtifactAlreadyRegistered( + @JsonProperty String artifactName, + @JsonProperty String groupId + ) implements Response { + + } + + @JsonSerialize + record GroupMissing(@JsonProperty("groupId") String s) implements Response { + + } + + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java new file mode 100644 index 00000000..065628bf --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java @@ -0,0 +1,49 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.Map; +import io.vavr.collection.SortedSet; +import org.spongepowered.downloads.api.ArtifactCoordinates; + +import java.io.Serializable; +import java.util.Optional; + +@JsonSerialize +public record GetArtifactDetailsResponse(Optional maybeArtifact) { + + @JsonSerialize + record RetrievedArtifact( + ArtifactCoordinates coordinates, + String displayName, + String website, + String gitRepository, + String issues, + Map> tags + ) implements Serializable { + + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java new file mode 100644 index 00000000..f22f221b --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java @@ -0,0 +1,40 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.transport; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.List; + +import java.io.Serializable; + + +@JsonSerialize +public record GetArtifactsResponse(@JsonProperty List artifactIds) implements Serializable { + @JsonCreator + public GetArtifactsResponse {} +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java new file mode 100644 index 00000000..e7815bb9 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java @@ -0,0 +1,56 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.Group; + +public final class GroupRegistration { + + @JsonDeserialize + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website + ) { + + @JsonCreator + public RegisterGroupRequest { } + + } + + public interface Response extends Jsonable { + + record GroupAlreadyRegistered(String groupNameRequested) implements Response { + } + + record GroupRegistered(Group group) implements Response { + + } + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java new file mode 100644 index 00000000..2a3e2790 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java @@ -0,0 +1,36 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.spongepowered.downloads.api.Group; + +import java.io.Serializable; +import java.util.Optional; + +@JsonSerialize +public record GroupResponse(Optional group) implements Serializable { + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java new file mode 100644 index 00000000..c2947a8a --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java @@ -0,0 +1,38 @@ +/* + * 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.transport; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.List; +import org.spongepowered.downloads.api.Group; + +@JsonSerialize +public record GroupsResponse( + @JsonProperty("groups") + List groups +) { + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java new file mode 100644 index 00000000..7d35e43f --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java @@ -0,0 +1,4 @@ +package org.spongepowered.downloads.routes; + +public class VersionRoutes { +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java new file mode 100644 index 00000000..b4723af2 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java @@ -0,0 +1,15 @@ +package org.spongepowered.downloads.versions; + +import akka.actor.typed.ActorRef; +import org.spongepowered.downloads.api.ArtifactCoordinates; +import org.spongepowered.downloads.api.MavenCoordinates; +import org.spongepowered.downloads.versions.transport.QueryLatest; +import org.spongepowered.downloads.versions.transport.QueryVersions; + +class VersionQueries { + + sealed interface Command { + record GetVersion(MavenCoordinates coordinates, ActorRef replyTo) implements Command {} + record GetVersions(ArtifactCoordinates coordinates, ActorRef replyTo) implements Command {} + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java new file mode 100644 index 00000000..0a44794b --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java @@ -0,0 +1,72 @@ +package org.spongepowered.downloads.versions; + + +import static akka.http.javadsl.server.Directives.complete; +import static akka.http.javadsl.server.Directives.concat; +import static akka.http.javadsl.server.Directives.get; +import static akka.http.javadsl.server.Directives.onSuccess; +import static akka.http.javadsl.server.Directives.pathPrefix; +import static akka.http.javadsl.server.PathMatchers.segment; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Scheduler; +import akka.actor.typed.javadsl.AskPattern; +import akka.http.javadsl.marshallers.jackson.Jackson; +import akka.http.javadsl.model.StatusCodes; +import akka.http.javadsl.server.PathMatchers; +import akka.http.javadsl.server.Route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.downloads.api.ArtifactCoordinates; +import org.spongepowered.downloads.versions.transport.QueryVersions; + +import java.time.Duration; +import java.util.concurrent.CompletionStage; + +public final class VersionRoutes { + + public static final Logger logger = LoggerFactory.getLogger( + org.spongepowered.downloads.artifacts.ArtifactRoutes.class); + + private final ActorRef artifactQueries; + private final Duration askTimeout; + private final Scheduler scheduler; + + public VersionRoutes(ActorSystem system, ActorRef artifactQueries) { + this.artifactQueries = artifactQueries; + scheduler = system.scheduler(); + askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); + } + + CompletionStage getVersions(ArtifactCoordinates coordinates) { + return AskPattern.ask(this.artifactQueries, ref -> new VersionQueries.Command.GetVersions( + coordinates, ref + ), this.askTimeout, this.scheduler); + } + + + /** + * This method creates one route (of possibly many more that will be part of your Web App) + */ + public Route artifactRoutes() { + // v1/groups + return pathPrefix( + segment("groups").slash(segment()).slash(segment("artifacts").slash(segment())), + (groupId, artifactId) -> + concat( + get(() -> onSuccess(this.getVersions(new ArtifactCoordinates(groupId, artifactId)), (resp) -> { + if (resp.size() <= 0) { + return complete(StatusCodes.NOT_FOUND); + } + return complete(StatusCodes.OK, resp, Jackson.marshaller()); + })), + path(PathMatchers.segment("versions").slash(), version -> get(() -> + onSuccess(this.getVersions(), resp -> { + return complete(StatusCodes.OK, resp, Jackson.marshaller()); + }))) + + ) + ); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java new file mode 100644 index 00000000..b8d61a97 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java @@ -0,0 +1,170 @@ +/* + * 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.versions.models; + +import org.hibernate.annotations.Immutable; +import org.spongepowered.downloads.artifact.api.MavenCoordinates; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.io.Serializable; +import java.util.Objects; + +@Entity(name = "TaggedVersion") +@Immutable +@Table(name = "versioned_tags", schema = "version") +@NamedQueries({ + @NamedQuery(name = "TaggedVersion.findAllForVersion", + query = + """ + select view from TaggedVersion view + where view.mavenGroupId = :groupId and view.mavenArtifactId = :artifactId and view.version = :version + """ + ), + @NamedQuery( + name = "TaggedVersion.findAllMatchingTagValues", + query = + """ + select view from TaggedVersion view + where view.mavenGroupId = :groupId + and view.mavenArtifactId = :artifactId + and view.tagName = :tagName + and view.tagValue like :tagValue + """ + ), + @NamedQuery( + name = "TaggedVersion.findMatchingTagValuesAndRecommendation", + query = + """ + select view from TaggedVersion view + where view.mavenGroupId = :groupId + and view.mavenArtifactId = :artifactId + and view.tagName = :tagName + and view.tagValue like :tagValue + and (view.versionView.recommended = :recommended or view.versionView.manuallyRecommended = :recommended) + """ + ) +}) +public class JpaTaggedVersion implements Serializable { + + @Id + @Column(name = "version_id", updatable = false) + private long versionId; + + @Id + @Column(updatable = false, name = "artifact_internal_id") + private long artifactId; + + @Id + @Column(name = "maven_group_id", updatable = false) + private String mavenGroupId; + + @Id + @Column(name = "maven_artifact_id", updatable = false) + private String mavenArtifactId; + + @Id + @Column(name = "maven_version", + updatable = false) + private String version; + + @Id + @Column(name = "tag_name", + updatable = false) + private String tagName; + + @Column(name = "tag_value", + updatable = false) + private String tagValue; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "maven_version", + referencedColumnName = "version", + nullable = false, + updatable = false, + insertable = false), + @JoinColumn(name = "maven_group_id", + referencedColumnName = "group_id", + nullable = false, + updatable = false, + insertable = false), + @JoinColumn(name = "maven_artifact_id", + referencedColumnName = "artifact_id", + nullable = false, + updatable = false, + insertable = false) + }) + private JpaVersionedArtifactView versionView; + + public String getTagName() { + return tagName; + } + + public String getTagValue() { + return tagValue; + } + + public MavenCoordinates asMavenCoordinates() { + return new MavenCoordinates(this.mavenGroupId, this.mavenArtifactId, this.version); + } + + public JpaVersionedArtifactView getVersion() { + return this.versionView; + } + + public String getMavenVersion() { + return this.version; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaTaggedVersion that = (JpaTaggedVersion) o; + return versionId == that.versionId && artifactId == that.artifactId && Objects.equals( + mavenGroupId, that.mavenGroupId) && Objects.equals( + mavenArtifactId, that.mavenArtifactId) && Objects.equals( + version, that.version) && Objects.equals(tagName, that.tagName) && Objects.equals( + tagValue, that.tagValue); + } + + @Override + public int hashCode() { + return Objects.hash(versionId, artifactId, mavenGroupId, mavenArtifactId, version, tagName, tagValue); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java new file mode 100644 index 00000000..5b9a2f40 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java @@ -0,0 +1,231 @@ +/* + * 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.versions.models; + +import io.vavr.Tuple; +import io.vavr.collection.HashMap; +import io.vavr.collection.List; +import io.vavr.collection.Map; +import org.hibernate.annotations.Immutable; +import org.spongepowered.downloads.artifact.api.Artifact; +import org.spongepowered.downloads.artifact.api.MavenCoordinates; +import org.spongepowered.downloads.versions.query.api.models.TagCollection; +import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import java.io.Serializable; +import java.net.URI; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +@Immutable +@Entity(name = "VersionedArtifactView") +@Table(name = "versioned_artifacts", + schema = "version") +@NamedQueries({ + @NamedQuery( + name = "VersionedArtifactView.count", + query = """ + select count(v) from VersionedArtifactView v + where v.groupId = :groupId and v.artifactId = :artifactId + """ + ), + @NamedQuery( + name = "VersionedArtifactView.recommendedCount", + query = """ + select count(v) from VersionedArtifactView v + where v.groupId = :groupId and v.artifactId = :artifactId and v.recommended = :recommended + """ + ), + @NamedQuery( + name = "VersionedArtifactView.findByArtifact", + query = """ + select v from VersionedArtifactView v where v.artifactId = :artifactId and v.groupId = :groupId + """ + ), + @NamedQuery( + name = "VersionedArtifactView.findByArtifactAndRecommendation", + query = """ + select v from VersionedArtifactView v + where v.artifactId = :artifactId and v.groupId = :groupId and (v.recommended = :recommended or v.manuallyRecommended = :recommended) + """ + ), + @NamedQuery( + name = "VersionedArtifactView.findExplicitly", + query = """ + select v from VersionedArtifactView v + left join fetch v.tags + where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version + """ + ), + @NamedQuery( + name = "VersionedArtifactView.findFullVersionDetails", + query = """ + select v from VersionedArtifactView v + left join fetch v.tags + left join fetch v.assets + where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version + """ + ) +}) +public class JpaVersionedArtifactView implements Serializable { + + @Id + @Column(name = "artifact_id", + updatable = false) + private String artifactId; + + @Id + @Column(name = "group_id", + updatable = false) + private String groupId; + + @Id + @Column(name = "version", + updatable = false) + private String version; + + @Column(name = "recommended") + private boolean recommended; + + @Column(name = "manual_recommendation") + private boolean manuallyRecommended; + + @Column(name = "ordering") + private int ordering; + + @OneToMany( + targetEntity = JpaTaggedVersion.class, + fetch = FetchType.LAZY, + cascade = CascadeType.ALL, + orphanRemoval = true, + mappedBy = "versionView") + private Set tags; + + @OneToMany( + targetEntity = JpaVersionedAsset.class, + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + orphanRemoval = true, + mappedBy = "versionView" + ) + private Set assets; + + @OneToOne( + targetEntity = JpaVersionedChangelog.class, + mappedBy = "versionView", + fetch = FetchType.LAZY + ) + private JpaVersionedChangelog changelog; + + public Set getTags() { + return tags; + } + + public String version() { + return this.version; + } + + public Map getTagValues() { + final var results = this.getTags(); + final var tuple2Stream = results.stream().map( + taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); + return tuple2Stream + .collect(HashMap.collector()); + } + + public TagCollection asTagCollection() { + final var results = this.getTags(); + final var tuple2Stream = results.stream().map( + taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); + return new TagCollection(tuple2Stream + .collect(HashMap.collector()), this.recommended); + } + + public boolean isRecommended() { + return this.recommended || this.manuallyRecommended; + } + + public MavenCoordinates asMavenCoordinates() { + return new MavenCoordinates(this.groupId + ":" + this.artifactId + ":" + this.version); + } + + Set getAssets() { + return assets; + } + + public List asArtifactList() { + return this.getAssets() + .stream() + .map( + asset -> new Artifact( + Optional.ofNullable(asset.getClassifier()), + URI.create(asset.getDownloadUrl()), + new String(asset.getMd5()), + new String(asset.getSha1()), + asset.getExtension() + ) + ).collect(List.collector()) + .sorted(Comparator.comparing(artifact -> artifact.classifier().orElse(""))); + } + + public Optional asVersionedCommit() { + final var changelog = this.changelog; + if (changelog == null) { + return Optional.empty(); + } + return Optional.of(changelog.getChangelog()); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaVersionedArtifactView that = (JpaVersionedArtifactView) o; + return Objects.equals(artifactId, that.artifactId) && Objects.equals( + groupId, that.groupId) && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, groupId, version); + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java new file mode 100644 index 00000000..3f3cd1d4 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java @@ -0,0 +1,146 @@ +/* + * 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.versions.models; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.IdClass; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Objects; + +@Immutable +@Entity(name = "VersionedAsset") +@Table(name = "artifact_versioned_assets", + schema = "version") +@IdClass(VersionedAssetID.class) +public class JpaVersionedAsset implements Serializable { + + @Id + @Column(name = "group_id") + private String groupId; + @Id + @Column(name = "artifact_id") + private String artifactId; + + @Id + @Column(name = "version") + private String version; + + @Id + @Column(name = "classifier") + private String classifier; + + @Id + @Column(name = "extension") + private String extension; + + @Column(name = "download_url") + private String downloadUrl; + + @Lob + @Type(type = "org.hibernate.type.BinaryType") + @Column(name = "md5") + private byte[] md5; + + @Lob + @Type(type = "org.hibernate.type.BinaryType") + @Column(name = "sha1") + private byte[] sha1; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "version", + referencedColumnName = "version", + nullable = false, + updatable = false, + insertable = false), + @JoinColumn(name = "group_id", + referencedColumnName = "group_id", + nullable = false, + updatable = false, + insertable = false), + @JoinColumn(name = "artifact_id", + referencedColumnName = "artifact_id", + nullable = false, + updatable = false, + insertable = false) + }) + private JpaVersionedArtifactView versionView; + + public String getClassifier() { + return classifier; + } + + public String getExtension() { + return extension; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public byte[] getMd5() { + return md5; + } + + public byte[] getSha1() { + return sha1; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaVersionedAsset that = (JpaVersionedAsset) o; + return Objects.equals(groupId, that.groupId) && Objects.equals( + artifactId, that.artifactId) && Objects.equals(version, that.version) && Objects.equals( + classifier, that.classifier) && Objects.equals(extension, that.extension) && Objects.equals( + downloadUrl, that.downloadUrl) && Arrays.equals(md5, that.md5) && Arrays.equals( + sha1, that.sha1) && Objects.equals(versionView, that.versionView); + } + + @Override + public int hashCode() { + int result = Objects.hash(groupId, artifactId, version, classifier, extension, downloadUrl, versionView); + result = 31 * result + Arrays.hashCode(md5); + result = 31 * result + Arrays.hashCode(sha1); + return result; + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java new file mode 100644 index 00000000..68396320 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java @@ -0,0 +1,140 @@ +/* + * 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.versions.models; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; +import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinColumns; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import java.io.Serializable; +import java.net.URL; +import java.util.Objects; + +@Immutable +@Entity(name = "VersionedChangelog") +@Table( + name = "versioned_changelogs", + schema = "version" +) +@TypeDefs({ + @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +}) +public class JpaVersionedChangelog implements Serializable { + + @Id + @Column(name = "version_id", updatable = false) + private String versionId; + + @Id + @Column(name = "group_id", updatable = false) + private String groupId; + + @Id + @Column(name = "artifact_id", updatable = false) + private String artifactId; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false), + @JoinColumn(name = "group_id", referencedColumnName = "group_id", insertable = false, updatable = false), + @JoinColumn(name = "artifact_id", referencedColumnName = "artifact_id", insertable = false, updatable = false) + }) + private JpaVersionedArtifactView versionView; + + @Column(name = "commit_sha", nullable = false) + private String sha; + + @Type(type = "jsonb") + @Column(name = "changelog", columnDefinition = "jsonb") + private VersionedChangelog changelog; + + @Column(name = "repo") + private URL repo; + + @Column(name = "branch") + private String branch; + + public String getSha() { + return sha; + } + + public VersionedChangelog getChangelog() { + return changelog; + } + + public URL getRepo() { + return repo; + } + + public String getBranch() { + return branch; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaVersionedChangelog that = (JpaVersionedChangelog) o; + return Objects.equals(versionId, that.versionId) && Objects.equals( + groupId, that.groupId) && Objects.equals(artifactId, that.artifactId) && Objects.equals( + versionView, that.versionView) && Objects.equals(sha, that.sha) && Objects.equals( + changelog, that.changelog) && Objects.equals(repo, that.repo) && Objects.equals( + branch, that.branch); + } + + @Override + public int hashCode() { + return Objects.hash(versionId, groupId, artifactId, versionView, sha, changelog, repo, branch); + } + + @Override + public String toString() { + return "JpaVersionedChangelog{" + + "versionId='" + versionId + '\'' + + ", groupId='" + groupId + '\'' + + ", artifactId='" + artifactId + '\'' + + ", versionView=" + versionView + + ", sha='" + sha + '\'' + + ", changelog=" + changelog + + ", repo=" + repo + + ", branch='" + branch + '\'' + + '}'; + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java new file mode 100644 index 00000000..3156c74d --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java @@ -0,0 +1,59 @@ +/* + * 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.versions.models; + +import java.io.Serializable; +import java.util.Objects; + +public class VersionedArtifactID implements Serializable { + private String artifactId; + + private String groupId; + + private String version; + + public VersionedArtifactID(final String artifactId, final String groupId, final String version) { + this.artifactId = artifactId; + this.groupId = groupId; + this.version = version; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + VersionedArtifactID that = (VersionedArtifactID) o; + return artifactId.equals(that.artifactId) && groupId.equals(that.groupId) && version.equals(that.version); + } + + @Override + public int hashCode() { + return Objects.hash(artifactId, groupId, version); + } +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java new file mode 100644 index 00000000..7bbba094 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java @@ -0,0 +1,12 @@ +package org.spongepowered.downloads.versions.models; + +import java.io.Serializable; + +public record VersionedAssetID( + String groupId, + String artifactId, + String version, + String classifier, + String extension + ) implements Serializable { +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java new file mode 100644 index 00000000..80d4a01d --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java @@ -0,0 +1,46 @@ +/* + * 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.versions.transport; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.List; +import io.vavr.collection.Map; +import org.spongepowered.downloads.artifact.api.Artifact; +import org.spongepowered.downloads.artifact.api.MavenCoordinates; + +import java.util.Optional; + +public interface QueryLatest { + + @JsonSerialize + record VersionInfo(MavenCoordinates coordinates, + List assets, + Map tagValues, + Optional commit, + boolean recommended + ) { + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java new file mode 100644 index 00000000..2ac34a11 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java @@ -0,0 +1,57 @@ +/* + * 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.versions.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.List; +import io.vavr.collection.Map; +import org.spongepowered.downloads.api.Artifact; +import org.spongepowered.downloads.api.MavenCoordinates; + +import java.util.Optional; + +public interface QueryVersions { + + @JsonSerialize + record VersionInfo(@JsonProperty Map artifacts, int offset, int limit, int size) { + + @JsonCreator + public VersionInfo { + } + } + + @JsonSerialize + record VersionDetails( + @JsonProperty("coordinates") MavenCoordinates coordinates, + @JsonProperty("commit") Optional commit, + @JsonProperty("assets") List components, + @JsonProperty("tags") Map tagValues, + @JsonProperty("recommended") boolean recommended + ) { + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java new file mode 100644 index 00000000..f740b7a8 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java @@ -0,0 +1,35 @@ +/* + * 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.versions.transport; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.vavr.collection.Map; + +@JsonSerialize +public record TagCollection( + Map tagValues, + boolean recommended +) { +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java new file mode 100644 index 00000000..efb8d8f0 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java @@ -0,0 +1,65 @@ +/* + * 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.versions.transport; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.collection.List; + +import java.net.URI; + +@JsonDeserialize +public final record VersionedChangelog( + List commits, + @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean processing +) { + + @JsonCreator + public VersionedChangelog { + } + + @JsonDeserialize + public final record IndexedCommit( + VersionedCommit commit, + List submoduleCommits + ) { + @JsonCreator + public IndexedCommit { + } + } + + @JsonDeserialize + public final record Submodule( + String name, + URI gitRepository, + List commits + ) { + @JsonCreator + public Submodule { + } + } + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java new file mode 100644 index 00000000..869d6888 --- /dev/null +++ b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java @@ -0,0 +1,69 @@ +/* + * 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.versions.transport; + + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.net.URI; +import java.time.ZonedDateTime; + +@JsonDeserialize +public record VersionedCommit( + String message, + String body, + String sha, + Author author, + Commiter commiter, + URI link, + ZonedDateTime commitDate +) { + + @JsonCreator + public VersionedCommit { + } + + @JsonDeserialize + public record Author( + String name, + String email + ) { + @JsonCreator + public Author { + } + } + + @JsonDeserialize + public record Commiter( + String name, + String email + ) { + @JsonCreator + public Commiter { + } + } +} + diff --git a/downloads-api/src/main/resources/application.conf b/downloads-api/src/main/resources/application.conf new file mode 100644 index 00000000..acd17dfa --- /dev/null +++ b/downloads-api/src/main/resources/application.conf @@ -0,0 +1,6 @@ +my-app { + routes { + # If ask takes more time than this to complete the request is failed + ask-timeout = 5s + } +} diff --git a/downloads-api/src/main/resources/logback.xml b/downloads-api/src/main/resources/logback.xml new file mode 100644 index 00000000..b1fe9ae9 --- /dev/null +++ b/downloads-api/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + + [%date{ISO8601}] [%level] [%logger] [%thread] [%X{akkaSource}] - %msg%n + + + + + 1024 + true + + + + + + + + diff --git a/downloads-api/src/test/java/com/example/UserRoutesTest.java b/downloads-api/src/test/java/com/example/UserRoutesTest.java new file mode 100644 index 00000000..02896cae --- /dev/null +++ b/downloads-api/src/test/java/com/example/UserRoutesTest.java @@ -0,0 +1,77 @@ +package com.example; + + +//#test-top +import akka.actor.typed.ActorRef; +import akka.http.javadsl.model.*; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import org.junit.*; +import org.junit.runners.MethodSorters; +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.StatusCodes; +import akka.actor.testkit.typed.javadsl.TestKitJunitResource; + + +//#set-up +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class UserRoutesTest extends JUnitRouteTest { + + @ClassRule + public static TestKitJunitResource testkit = new TestKitJunitResource(); + + //#test-top + // shared registry for all tests + private static ActorRef userRegistry; + private TestRoute appRoute; + + @BeforeClass + public static void beforeClass() { + userRegistry = testkit.spawn(UserRegistry.create()); + } + + @Before + public void before() { + UserRoutes userRoutes = new UserRoutes(testkit.system(), userRegistry); + appRoute = testRoute(userRoutes.userRoutes()); + } + + @AfterClass + public static void afterClass() { + testkit.stop(userRegistry); + } + + //#set-up + //#actual-test + @Test + public void test1NoUsers() { + appRoute.run(HttpRequest.GET("/users")) + .assertStatusCode(StatusCodes.OK) + .assertMediaType("application/json") + .assertEntity("{\"users\":[]}"); + } + + //#actual-test + //#testing-post + @Test + public void test2HandlePOST() { + appRoute.run(HttpRequest.POST("/users") + .withEntity(MediaTypes.APPLICATION_JSON.toContentType(), + "{\"name\": \"Kapi\", \"age\": 42, \"countryOfResidence\": \"jp\"}")) + .assertStatusCode(StatusCodes.CREATED) + .assertMediaType("application/json") + .assertEntity("{\"description\":\"User Kapi created.\"}"); + } + //#testing-post + + @Test + public void test3Remove() { + appRoute.run(HttpRequest.DELETE("/users/Kapi")) + .assertStatusCode(StatusCodes.OK) + .assertMediaType("application/json") + .assertEntity("{\"description\":\"User Kapi deleted.\"}"); + + } + //#set-up +} +//#set-up diff --git a/downloads-api/src/test/resources/application-test.conf b/downloads-api/src/test/resources/application-test.conf new file mode 100644 index 00000000..7f763324 --- /dev/null +++ b/downloads-api/src/test/resources/application-test.conf @@ -0,0 +1,3 @@ +include "application" + +# default config for tests, we just import the regular conf \ No newline at end of file diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java index 8826c6c4..a4c63b9d 100644 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java +++ b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java @@ -56,19 +56,19 @@ default AggregateEventTagger aggregateTag() { } @JsonTypeName("repository-registered") - final record RepositoryRegistered(URI repository) implements GitEvent { + record RepositoryRegistered(URI repository) implements GitEvent { @JsonCreator public RepositoryRegistered { } } @JsonTypeName("commit-extracted") - final record CommitRegistered(MavenCoordinates coordinates, String commit) implements GitEvent { + record CommitRegistered(MavenCoordinates coordinates, String commit) implements GitEvent { @JsonCreator public CommitRegistered { } } @JsonTypeName("commit-resolved") - final record CommitResolved(MavenCoordinates coordinates, VersionedCommit resolvedCommit) implements GitEvent { + record CommitResolved(MavenCoordinates coordinates, VersionedCommit resolvedCommit) implements GitEvent { @JsonCreator public CommitResolved { } diff --git a/version-synchronizer/src/main/resources/logback.xml b/version-synchronizer/src/main/resources/logback.xml index ad0d23f6..8ca52f08 100644 --- a/version-synchronizer/src/main/resources/logback.xml +++ b/version-synchronizer/src/main/resources/logback.xml @@ -17,8 +17,8 @@ - - + + diff --git a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java index 01d80d00..f431e2e8 100644 --- a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java +++ b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java @@ -100,4 +100,21 @@ public void verifyNonExistentCommit() { )); probe.fishForMessage(Duration.ofSeconds(60), m -> FishingOutcomes.complete()); } + + @Test + public void verifyCommitFromTwoRepositories() { + final var teller = testKit.createTestProbe(CommitDetailsRegistrar.Command.class); + final var actor = testKit.spawn(CommitResolutionManager.resolveCommit(teller.ref())); + final var coords = new ArtifactCoordinates("org.spongepowered", "spongevanilla").version("1.16.5-8.1.0-RC1184"); + final var commit = "6e443ec04ded4385d12c2e609360e81a770fbfcb"; + final var url = "https://github.com/spongepowered/spongevanilla.git"; + final var newUrl = "https://github.com/spongepowered/sponge.git"; + final var repos = List.of(URI.create(newUrl), URI.create(url)); + final var probe = testKit.createTestProbe(); + final var replyTo = probe.ref(); + actor.tell(new CommitResolutionManager.ResolveCommitDetails( + coords, commit, repos, replyTo + )); + probe.fishForMessage(Duration.ofMinutes(10), m -> FishingOutcomes.complete()); + } } From 8f5deb91f2a1e1be0dd12dec3ce9a21f4aca706f Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Fri, 3 Feb 2023 00:36:16 -0800 Subject: [PATCH 3/9] start migration to micronaut Signed-off-by: Gabriel Harris-Rouquette --- .creds | 2 + .github/workflows/graalvm.yml | 35 +++ .github/workflows/gradle.yml | 34 +++ .java-version | 2 +- .jpb/jpb-settings.xml | 10 + .jpb/persistence-units.xml | 12 + .jvmopts | 2 +- akka/build.gradle.kts | 29 +++ akka/gradle.properties | 0 akka/gradlew | 240 ++++++++++++++++++ akka/gradlew.bat | 91 +++++++ akka/settings.gradle | 3 + akka/src/main/java/module-info.java | 5 + .../downloads/akka/AkkaExtension.java | 46 ++++ .../artifact/global/GlobalManager.java | 1 - artifacts/.github/workflows/gradle.yml | 31 +++ artifacts/.gitignore | 15 ++ artifacts/README.md | 87 +++++++ artifacts/api/build.gradle.kts | 9 + .../downloads/artifact/api/Artifact.java | 46 ++++ .../artifact/api/ArtifactCollection.java | 43 ++++ .../artifact/api/ArtifactCoordinates.java | 66 +++++ .../artifact/api/ArtifactService.java | 83 ++++++ .../downloads/artifact/api/Group.java | 42 +++ .../artifact/api/MavenCoordinates.java | 192 ++++++++++++++ .../downloads/artifact/api/VersionType.java | 115 +++++++++ .../artifact/api/query/ArtifactDetails.java | 100 ++++++++ .../api/query/ArtifactRegistration.java | 82 ++++++ .../api/query/GetArtifactsResponse.java | 60 +++++ .../artifact/api/query/GroupRegistration.java | 55 ++++ .../artifact/api/query/GroupResponse.java | 61 +++++ .../artifact/api/query/GroupsResponse.java | 48 ++++ artifacts/build.gradle.kts | 18 ++ artifacts/events/build.gradle.kts | 14 + .../artifacts/events/ArtifactEvent.java | 74 ++++++ .../artifacts/events/GroupUpdate.java | 73 ++++++ artifacts/gradle.properties | 1 + artifacts/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + artifacts/gradlew | 240 ++++++++++++++++++ artifacts/gradlew.bat | 91 +++++++ artifacts/micronaut-cli.yml | 6 + artifacts/server/build.gradle.kts | 79 ++++++ .../artifacts/server/Application.java | 48 ++++ .../server/details/ArtifactDetailsEntity.java | 213 ++++++++++++++++ .../server/details/DetailsCommand.java | 114 +++++++++ .../server/details/DetailsEvent.java | 109 ++++++++ .../server/details/DetailsManager.java | 136 ++++++++++ .../server/details/state/DetailsState.java | 43 ++++ .../server/details/state/EmptyState.java | 68 +++++ .../server/details/state/PopulatedState.java | 85 +++++++ .../server/global/GlobalCommand.java | 62 +++++ .../artifacts/server/global/GlobalEvent.java | 53 ++++ .../server/global/GlobalManager.java | 54 ++++ .../server/global/GlobalRegistration.java | 99 ++++++++ .../artifacts/server/global/GlobalState.java | 38 +++ .../artifacts/server/groups/GroupCommand.java | 63 +++++ .../artifacts/server/groups/GroupEntity.java | 206 +++++++++++++++ .../artifacts/server/groups/GroupEvent.java | 124 +++++++++ .../artifacts/server/groups/GroupManager.java | 100 ++++++++ .../server/groups/GroupsQueryController.java | 65 +++++ .../server/groups/state/EmptyState.java | 62 +++++ .../server/groups/state/GroupState.java | 47 ++++ .../server/groups/state/PopulatedState.java | 51 ++++ .../server/query/ArtifactsQuery.java | 12 + .../src/main/resources/application.toml | 10 + .../server/src/main/resources/bootstrap.toml | 4 + .../server/src/main/resources/logback.xml | 15 ++ .../artifacts/server/Application.java | 20 ++ .../server/query/ArtifactsQuery.java | 9 + .../server/query/GroupsQueryController.java | 28 ++ artifacts/src/main/resources/application.toml | 10 + artifacts/src/main/resources/bootstrap.toml | 4 + artifacts/src/main/resources/logback.xml | 15 ++ .../downloads/artifacts/ArtifactsTest.java | 21 ++ artifacts/worker/build.gradle.kts | 47 ++++ .../artifacts/worker/Application.java | 33 +++ build.gradle.kts | 117 +++++++++ build.sbt | 129 ---------- .../kotlin/soad.java-conventions.gradle.kts | 20 ++ .../downloads/artifacts/ArtifactQueries.java | 11 +- .../downloads/artifacts/ArtifactRoutes.java | 9 +- gradle.properties | 7 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 240 ++++++++++++++++++ gradlew.bat | 91 +++++++ micronaut-cli.yml | 6 + openapi.properties | 6 + settings.gradle.kts | 32 +++ .../java/systemofadownload/AkkaExtension.java | 56 ++++ .../java/systemofadownload/Application.java | 18 ++ .../SystemofadownloadController.java | 58 +++++ .../UnauthorizedHandler.java | 30 +++ .../artifacts/ArtifactController.java | 33 +++ .../artifacts/api/Artifact.java | 46 ++++ .../artifacts/api/ArtifactCollection.java | 43 ++++ .../artifacts/api/ArtifactCoordinates.java | 66 +++++ .../artifacts/api/ArtifactService.java | 69 +++++ .../artifacts/api/Group.java | 42 +++ .../artifacts/api/MavenCoordinates.java | 192 ++++++++++++++ .../artifacts/api/VersionType.java | 115 +++++++++ .../artifacts/api/event/ArtifactUpdate.java | 110 ++++++++ .../artifacts/api/event/GroupUpdate.java | 74 ++++++ .../artifacts/api/query/ArtifactDetails.java | 129 ++++++++++ .../api/query/ArtifactRegistration.java | 81 ++++++ .../api/query/GetArtifactsResponse.java | 60 +++++ .../api/query/GroupRegistration.java | 55 ++++ .../artifacts/api/query/GroupResponse.java | 61 +++++ .../artifacts/api/query/GroupsResponse.java | 48 ++++ .../query/ArtifactQueryController.java | 23 ++ .../groups/GroupController.java | 41 +++ .../groups/query/GroupsQueryController.java | 32 +++ src/main/resources/application.toml | 20 ++ src/main/resources/db/changelog/01-schema.xml | 10 + src/main/resources/db/liquibase-changelog.xml | 8 + src/main/resources/logback.xml | 15 ++ .../SystemofadownloadTest.java | 21 ++ terraform/app/locals.tf | 18 +- terraform/app/main.tf | 1 + .../worker/CommitDetailsRegistrarTest.java | 6 + 121 files changed, 6424 insertions(+), 146 deletions(-) create mode 100644 .creds create mode 100644 .github/workflows/graalvm.yml create mode 100644 .github/workflows/gradle.yml create mode 100644 .jpb/jpb-settings.xml create mode 100644 .jpb/persistence-units.xml create mode 100644 akka/build.gradle.kts create mode 100644 akka/gradle.properties create mode 100755 akka/gradlew create mode 100644 akka/gradlew.bat create mode 100644 akka/settings.gradle create mode 100644 akka/src/main/java/module-info.java create mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java create mode 100644 artifacts/.github/workflows/gradle.yml create mode 100644 artifacts/.gitignore create mode 100644 artifacts/README.md create mode 100644 artifacts/api/build.gradle.kts create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java create mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java create mode 100644 artifacts/build.gradle.kts create mode 100644 artifacts/events/build.gradle.kts create mode 100644 artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java create mode 100644 artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java create mode 100644 artifacts/gradle.properties create mode 100644 artifacts/gradle/wrapper/gradle-wrapper.jar create mode 100644 artifacts/gradle/wrapper/gradle-wrapper.properties create mode 100755 artifacts/gradlew create mode 100644 artifacts/gradlew.bat create mode 100644 artifacts/micronaut-cli.yml create mode 100644 artifacts/server/build.gradle.kts create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java create mode 100644 artifacts/server/src/main/resources/application.toml create mode 100644 artifacts/server/src/main/resources/bootstrap.toml create mode 100644 artifacts/server/src/main/resources/logback.xml create mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java create mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java create mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java create mode 100644 artifacts/src/main/resources/application.toml create mode 100644 artifacts/src/main/resources/bootstrap.toml create mode 100644 artifacts/src/main/resources/logback.xml create mode 100644 artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java create mode 100644 artifacts/worker/build.gradle.kts create mode 100644 artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/Application.java create mode 100644 build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 micronaut-cli.yml create mode 100644 openapi.properties create mode 100644 settings.gradle.kts create mode 100644 src/main/java/systemofadownload/AkkaExtension.java create mode 100644 src/main/java/systemofadownload/Application.java create mode 100644 src/main/java/systemofadownload/SystemofadownloadController.java create mode 100644 src/main/java/systemofadownload/UnauthorizedHandler.java create mode 100644 src/main/java/systemofadownload/artifacts/ArtifactController.java create mode 100644 src/main/java/systemofadownload/artifacts/api/Artifact.java create mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java create mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java create mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactService.java create mode 100644 src/main/java/systemofadownload/artifacts/api/Group.java create mode 100644 src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java create mode 100644 src/main/java/systemofadownload/artifacts/api/VersionType.java create mode 100644 src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java create mode 100644 src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java create mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java create mode 100644 src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java create mode 100644 src/main/java/systemofadownload/groups/GroupController.java create mode 100644 src/main/java/systemofadownload/groups/query/GroupsQueryController.java create mode 100644 src/main/resources/application.toml create mode 100644 src/main/resources/db/changelog/01-schema.xml create mode 100644 src/main/resources/db/liquibase-changelog.xml create mode 100644 src/main/resources/logback.xml create mode 100644 src/test/java/systemofadownload/SystemofadownloadTest.java create mode 100644 version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java diff --git a/.creds b/.creds new file mode 100644 index 00000000..9c278eea --- /dev/null +++ b/.creds @@ -0,0 +1,2 @@ +baz=fdr +foo=asddr diff --git a/.github/workflows/graalvm.yml b/.github/workflows/graalvm.yml new file mode 100644 index 00000000..803e650c --- /dev/null +++ b/.github/workflows/graalvm.yml @@ -0,0 +1,35 @@ +name: GraalVM CE CI +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-test-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle-test- + - name: Setup GraalVM CE + uses: DeLaGuardo/setup-graalvm@3.1 + with: + graalvm-version: 22.3.0.java17 + - name: Install Native Image + run: gu install native-image + - name: Docker login + uses: docker/login-action@v1 + with: + registry: ${{ secrets.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build And Push Docker Image + env: + DOCKER_REPOSITORY_PATH: ${{ secrets.DOCKER_REPOSITORY_PATH }} + DOCKER_REGISTRY_URL: ${{ secrets.DOCKER_REGISTRY_URL }} + TESTCONTAINERS_RYUK_DISABLED: true + run: | + export DOCKER_IMAGE=`echo "${DOCKER_REGISTRY_URL}/${DOCKER_REPOSITORY_PATH}/systemofadownload" | sed -e 's#//#/#' -e 's#^/##'` + ./gradlew check dockerPushNative --no-daemon diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..1dc46d08 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,34 @@ +name: Java CI +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + ~/.m2/repository + key: ${{ runner.os }}-gradle-test-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle-test- + - name: Set up JDK 17 + uses: actions/setup-java@v1 + with: + java-version: 17 + - name: Docker login + uses: docker/login-action@v1 + with: + registry: ${{ secrets.DOCKER_REGISTRY_URL }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build And Push Docker Image + env: + DOCKER_REPOSITORY_PATH: ${{ secrets.DOCKER_REPOSITORY_PATH }} + DOCKER_REGISTRY_URL: ${{ secrets.DOCKER_REGISTRY_URL }} + TESTCONTAINERS_RYUK_DISABLED: true + run: | + export DOCKER_IMAGE=`echo "${DOCKER_REGISTRY_URL}/${DOCKER_REPOSITORY_PATH}/systemofadownload" | sed -e 's#//#/#' -e 's#^/##'` + ./gradlew check dockerPush --no-daemon diff --git a/.java-version b/.java-version index 98d9bcb7..46cbfbc7 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17 +graalvm64-17.0.4 diff --git a/.jpb/jpb-settings.xml b/.jpb/jpb-settings.xml new file mode 100644 index 00000000..57462660 --- /dev/null +++ b/.jpb/jpb-settings.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.jpb/persistence-units.xml b/.jpb/persistence-units.xml new file mode 100644 index 00000000..a462c23d --- /dev/null +++ b/.jpb/persistence-units.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.jvmopts b/.jvmopts index d63f5e5e..b7359b71 100644 --- a/.jvmopts +++ b/.jvmopts @@ -3,4 +3,4 @@ -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 ---add-opens=java.base/java.lang=ALL-UNNAMED \ No newline at end of file +--add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/akka/build.gradle.kts b/akka/build.gradle.kts new file mode 100644 index 00000000..4ab025b9 --- /dev/null +++ b/akka/build.gradle.kts @@ -0,0 +1,29 @@ + +version = "0.1" +group = "org.spongepowered.downloads" + + +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project + +dependencies { + implementation("com.ongres.scram:client:2.1") + implementation("jakarta.annotation:jakarta.annotation-api") + implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) + implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") + implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") + + runtimeOnly("ch.qos.logback:logback-classic") + compileOnly("org.graalvm.nativeimage:svm") + + implementation("io.micronaut:micronaut-validation") +} + + diff --git a/akka/gradle.properties b/akka/gradle.properties new file mode 100644 index 00000000..e69de29b diff --git a/akka/gradlew b/akka/gradlew new file mode 100755 index 00000000..a69d9cb6 --- /dev/null +++ b/akka/gradlew @@ -0,0 +1,240 @@ +#!/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 new file mode 100644 index 00000000..f127cfd4 --- /dev/null +++ b/akka/gradlew.bat @@ -0,0 +1,91 @@ +@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/settings.gradle b/akka/settings.gradle new file mode 100644 index 00000000..766979b3 --- /dev/null +++ b/akka/settings.gradle @@ -0,0 +1,3 @@ + +rootProject.name="akka" + diff --git a/akka/src/main/java/module-info.java b/akka/src/main/java/module-info.java new file mode 100644 index 00000000..4b98e64f --- /dev/null +++ b/akka/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module systemofadownload.akka { + requires akka.actor.typed; + requires akka.cluster.sharding; + exports org.spongepowered.downloads.akka; +} diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java new file mode 100644 index 00000000..1a232f7d --- /dev/null +++ b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java @@ -0,0 +1,46 @@ +package org.spongepowered.downloads.akka; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Scheduler; +import akka.actor.typed.SpawnProtocol; +import akka.actor.typed.javadsl.Adapter; +import akka.actor.typed.javadsl.Behaviors; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.management.cluster.bootstrap.ClusterBootstrap; +import akka.management.javadsl.AkkaManagement; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; + +@Factory +public class AkkaExtension { + + @Bean + public Scheduler systemScheduler() { + return system().scheduler(); + } + + @Bean + public Config akkaConfig() { + return ConfigFactory.load(); + } + + @Bean(preDestroy = "terminate") + public ActorSystem system() { + Config config = akkaConfig(); + return ActorSystem.create( + Behaviors.setup(ctx -> { + akka.actor.ActorSystem unTypedSystem = Adapter.toClassic(ctx.getSystem()); + AkkaManagement.get(unTypedSystem).start(); + ClusterBootstrap.get(unTypedSystem).start(); + return SpawnProtocol.create(); + }), config.getString("some.cluster.name")); + } + + @Bean + public ClusterSharding clusterSharding() { + return ClusterSharding.get(system()); + } + +} diff --git a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java index 804c7c39..db3bbc2a 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java +++ b/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/global/GlobalManager.java @@ -30,7 +30,6 @@ public GlobalManager(final ClusterSharding clusterSharding) { ); } - public CompletionStage registerGroup( GroupRegistration.Response.GroupRegistered registered ) { diff --git a/artifacts/.github/workflows/gradle.yml b/artifacts/.github/workflows/gradle.yml new file mode 100644 index 00000000..975d836e --- /dev/null +++ b/artifacts/.github/workflows/gradle.yml @@ -0,0 +1,31 @@ +name: Java CI with Gradle (Groovy) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: temurin + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1.0.4 + - name: Build with Gradle + uses: gradle/gradle-build-action@v2.2.0 + with: + arguments: build + - uses: actions/upload-artifact@v3.1.0 + with: + name: Build Artifacts + path: | + **/build/reports diff --git a/artifacts/.gitignore b/artifacts/.gitignore new file mode 100644 index 00000000..5a03bc30 --- /dev/null +++ b/artifacts/.gitignore @@ -0,0 +1,15 @@ +Thumbs.db +.DS_Store +.gradle +build/ +target/ +out/ +.micronaut/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +.factorypath diff --git a/artifacts/README.md b/artifacts/README.md new file mode 100644 index 00000000..47a547a0 --- /dev/null +++ b/artifacts/README.md @@ -0,0 +1,87 @@ +## Micronaut 3.8.2 Documentation + +- [User Guide](https://docs.micronaut.io/3.8.2/guide/index.html) +- [API Reference](https://docs.micronaut.io/3.8.2/api/index.html) +- [Configuration Reference](https://docs.micronaut.io/3.8.2/guide/configurationreference.html) +- [Micronaut Guides](https://guides.micronaut.io/index.html) +--- + +- [Shadow Gradle Plugin](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) +## Feature github-workflow-ci documentation + +- [https://docs.github.com/en/actions](https://docs.github.com/en/actions) + + +## Feature test-resources documentation + +- [Micronaut Test Resources documentation](https://micronaut-projects.github.io/micronaut-test-resources/latest/guide/) + + +## Feature micronaut-aot documentation + +- [Micronaut AOT documentation](https://micronaut-projects.github.io/micronaut-aot/latest/guide/) + + +## Feature security-ldap documentation + +- [Micronaut Security LDAP documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html#ldap) + + +## Feature security-jwt documentation + +- [Micronaut Security JWT documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html) + + +## Feature data-r2dbc documentation + +- [Micronaut Data R2DBC documentation](https://micronaut-projects.github.io/micronaut-data/latest/guide/#dbc) + +- [https://r2dbc.io](https://r2dbc.io) + + +## Feature kafka documentation + +- [Micronaut Kafka Messaging documentation](https://micronaut-projects.github.io/micronaut-kafka/latest/guide/index.html) + + +## Feature openapi documentation + +- [Micronaut OpenAPI Support documentation](https://micronaut-projects.github.io/micronaut-openapi/latest/guide/index.html) + +- [https://www.openapis.org](https://www.openapis.org) + + +## Feature cache-caffeine documentation + +- [Micronaut Caffeine Cache documentation](https://micronaut-projects.github.io/micronaut-cache/latest/guide/index.html) + +- [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + + +## Feature r2dbc documentation + +- [Micronaut R2DBC documentation](https://micronaut-projects.github.io/micronaut-r2dbc/latest/guide/) + +- [https://r2dbc.io](https://r2dbc.io) + + +## Feature junit-params documentation + +- [https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests) + + +## Feature discovery-kubernetes documentation + +- [Micronaut Kubernetes Service Discovery documentation](https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/#service-discovery) + + +## Feature http-client documentation + +- [Micronaut HTTP Client documentation](https://docs.micronaut.io/latest/guide/index.html#httpClient) + + +## Feature serialization-jackson documentation + +- [Micronaut Serialization Jackson Core documentation](https://micronaut-projects.github.io/micronaut-serialization/latest/guide/) + + diff --git a/artifacts/api/build.gradle.kts b/artifacts/api/build.gradle.kts new file mode 100644 index 00000000..399e5cbf --- /dev/null +++ b/artifacts/api/build.gradle.kts @@ -0,0 +1,9 @@ + +val jacksonVersion:String by project +dependencies { + api(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) + api("com.fasterxml.jackson:jackson-core") + api("com.fasterxml.jackson.core:jackson-databind") + api("com.fasterxml.jackson.core:jackson-annotations") + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java new file mode 100644 index 00000000..94c7ef77 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java @@ -0,0 +1,46 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.net.URI; +import java.util.Optional; + +@JsonSerialize +public final record Artifact( + @JsonProperty Optional classifier, + @JsonProperty URI downloadUrl, + @JsonProperty String md5, + @JsonProperty String sha1, + @JsonProperty String extension +) { + @JsonCreator + public Artifact { + } + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java new file mode 100644 index 00000000..9962d09f --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java @@ -0,0 +1,43 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; + +@JsonDeserialize +public final record ArtifactCollection( + @JsonProperty("assets") List components, + @JsonProperty("coordinates") MavenCoordinates coordinates +) { + + @JsonCreator + public ArtifactCollection { + } + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java new file mode 100644 index 00000000..99131c11 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java @@ -0,0 +1,66 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.StringJoiner; + +@JsonDeserialize +public record ArtifactCoordinates( + @JsonProperty(required = true) String groupId, + @JsonProperty(required = true) String artifactId +) { + @JsonCreator + public ArtifactCoordinates { + } + + public MavenCoordinates version(String version) { + return MavenCoordinates.parse( + new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); + } + + public String asMavenString() { + return this.groupId() + ":" + this.artifactId(); + } + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String groupId() { + return groupId; + } + + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String artifactId() { + return artifactId; + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java new file mode 100644 index 00000000..00508fc5 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java @@ -0,0 +1,83 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import akka.NotUsed; +import com.lightbend.lagom.javadsl.api.Descriptor; +import com.lightbend.lagom.javadsl.api.Service; +import com.lightbend.lagom.javadsl.api.ServiceCall; +import com.lightbend.lagom.javadsl.api.broker.Topic; +import com.lightbend.lagom.javadsl.api.broker.kafka.KafkaProperties; +import com.lightbend.lagom.javadsl.api.transport.Method; +import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; +import org.spongepowered.downloads.artifact.api.event.GroupUpdate; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +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.query.GroupsResponse; + +public interface ArtifactService extends Service { + + ServiceCall getArtifacts(String groupId); + + ServiceCall registerArtifacts( + String groupId + ); + + ServiceCall registerGroup(); + + ServiceCall, ArtifactDetails.Response> updateDetails(String groupId, String artifactId); + + ServiceCall getGroup(String groupId); + + ServiceCall getGroups(); + + Topic groupTopic(); + + Topic artifactUpdate(); + + @Override + default Descriptor descriptor() { + return Service.named("artifacts") + .withCalls( + Service.restCall(Method.GET, "/artifacts/groups/:groupId", this::getGroup), + Service.restCall(Method.GET, "/artifacts/groups", this::getGroups), + Service.restCall(Method.POST, "/artifacts/groups", this::registerGroup), + Service.restCall(Method.GET, "/artifacts/groups/:groupId/artifacts", this::getArtifacts), + Service.restCall(Method.POST, "/artifacts/groups/:groupId/artifacts", this::registerArtifacts), + Service.restCall(Method.PATCH, "/artifacts/groups/:groupId/artifacts/:artifactId/update", this::updateDetails) + ) + .withTopics( + Service.topic("group-activity", this::groupTopic) + .withProperty(KafkaProperties.partitionKeyStrategy(), GroupUpdate::groupId), + Service.topic("artifact-details-update", this::artifactUpdate) + .withProperty(KafkaProperties.partitionKeyStrategy(), ArtifactUpdate::partitionKey) + ) + .withAutoAcl(true); + } + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java new file mode 100644 index 00000000..7e9145d1 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java @@ -0,0 +1,42 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public record Group( + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String website +) { + + @JsonCreator + public Group { + } + +} 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 new file mode 100644 index 00000000..c1acb521 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java @@ -0,0 +1,192 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +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; +import java.util.regex.Pattern; + +@JsonDeserialize +public final class MavenCoordinates implements Comparable { + + private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String groupId; + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String artifactId; + /** + * The version of an artifact, as defined by the Apache Maven documentation. This is + * traditionally specified as a Maven repository searchable version string, such as + * {@code 1.0.0-SNAPSHOT}. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String version; + + @JsonIgnore + public final VersionType versionType; + + @JsonIgnore + private final ComparableVersion mavenVersion; + + /** + * Parses a set of maven formatted coordinates as per + * Apache + * Maven's documentation. + * + * @param coordinates The coordinates delimited by `:` + * @return A parsed set of MavenCoordinates + */ + public static MavenCoordinates parse(final String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + return new MavenCoordinates(groupId, artifactId, version); + } + + public MavenCoordinates(String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonCreator + public MavenCoordinates( + @JsonProperty("groupId") final String groupId, + @JsonProperty("artifactId") final String artifactId, + @JsonProperty("version") final String version + ) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonIgnore + public String asStandardCoordinates() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.versionType.asStandardVersionString(this.version)) + .toString(); + } + + @JsonIgnore + public boolean isSnapshot() { + return this.versionType.isSnapshot(); + } + + @JsonIgnore + public ArtifactCoordinates asArtifactCoordinates() { + return new ArtifactCoordinates(this.groupId, this.artifactId); + } + + @Override + public String toString() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.version) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final MavenCoordinates that = (MavenCoordinates) o; + return Objects.equals(this.groupId, that.groupId) && Objects.equals( + this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(this.groupId, this.artifactId, this.version); + } + + @Override + public int compareTo(final MavenCoordinates o) { + final var group = this.groupId.compareTo(o.groupId); + if (group != 0) { + return group; + } + final var artifact = this.artifactId.compareTo(o.artifactId); + if (artifact != 0) { + return artifact; + } + return this.mavenVersion.compareTo(o.mavenVersion); + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java new file mode 100644 index 00000000..471c4a5c --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java @@ -0,0 +1,115 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api; + +import java.util.StringJoiner; +import java.util.regex.Pattern; + +/** + * In conjunction with {@link MavenCoordinates}, can be used to determine the + * version type of the coordinates, and whether + */ +public enum VersionType { + /** + * A timestamp based file snapshot, such as {@code 1.0.0-20210118.163210-1} + * to where it can be interpreted that the {@link #SNAPSHOT snapshot} version + * would be {@code 1.0.0-SNAPSHOT} that happened to build at date time + * {@code January 18th, 2021 at 16h32m10s} and it's the first build. + */ + TIMESTAMP_SNAPSHOT { + @Override + public boolean isSnapshot() { + return true; + } + + @Override + public String asStandardVersionString(final String version) { + final var split = version.split("-"); + final var stringJoiner = new StringJoiner("-"); + for (int i = 0; i < split.length - 2; i++) { + stringJoiner.add(split[i]); + } + + return stringJoiner.add(SNAPSHOT_VERSION).toString(); + } + }, + + /** + * A standard generic snapshot relative version of a release, such as {@code 1.0.0-SNAPSHOT}. + */ + SNAPSHOT { + @Override + public boolean isSnapshot() { + return true; + } + }, + + /** + * A standard release version not abiding by any snapshot guidelines, considered + * final and singular, such as {@code 1.0.0} + */ + RELEASE; + + /* + Simple SNAPSHOT placeholder + */ + private static final String SNAPSHOT_VERSION = "SNAPSHOT"; + + /* + Verifies the pattern that the snapshot version is date.time-build formatted, + enables the pattern match for a timestamped snapshot + */ + private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-(\\d{8}.\\d{6})-(\\d+)$"); + + private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("(\\d{8}.\\d{6})-(\\d+)$"); + + public static VersionType fromVersion(final String version) { + if (version == null || version.isEmpty()) { + throw new IllegalArgumentException("Version cannot be empty"); + } + // Simple check to find out if the version ends with SNAPSHOT. + if (version.regionMatches( + true, + version.length() - SNAPSHOT_VERSION.length(), + SNAPSHOT_VERSION, + 0, + SNAPSHOT_VERSION.length() + )) { + return SNAPSHOT; + } + if (VERSION_FILE_PATTERN.matcher(version).matches()) { + return TIMESTAMP_SNAPSHOT; + } + return RELEASE; + } + + public boolean isSnapshot() { + return false; + } + + public String asStandardVersionString(final String version) { + return version; + } +} 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 new file mode 100644 index 00000000..27247571 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java @@ -0,0 +1,100 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.net.URL; + +public final class ArtifactDetails { + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonDeserialize + public sealed interface Update { + + @JsonTypeName("website") + record Website( + @JsonProperty(required = true) String website + ) implements Update { + + @JsonCreator + public Website { + } + + } + + @JsonTypeName("displayName") + record DisplayName( + @JsonProperty(required = true) String display + ) implements Update { + + @JsonCreator + public DisplayName { + } + + } + + @JsonTypeName("issues") + record Issues( + @JsonProperty(required = true) String issues + ) implements Update { + @JsonCreator + public Issues { + } + + } + + @JsonTypeName("git-repo") + record GitRepository( + @JsonProperty(required = true) String gitRepo + ) implements Update { + + @JsonCreator + public GitRepository { + } + + } + } + + @JsonSerialize + public record Response( + String name, + String displayName, + String website, + String issues, + String gitRepo + ) { + + } + + +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java new file mode 100644 index 00000000..362cdd88 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java @@ -0,0 +1,82 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +public final class ArtifactRegistration { + + @JsonSerialize + public record RegisterArtifact( + @JsonProperty(required = true) String artifactId, + @JsonProperty(required = true) String displayName + ) { + + @JsonCreator + public RegisterArtifact(final String artifactId, final String displayName) { + this.artifactId = artifactId; + this.displayName = displayName; + } + + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Response.GroupMissing.class, + name = "UnknownGroup"), + @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, + name = "RegisteredArtifact"), + @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, + name = "AlreadyRegistered"), + }) + public sealed interface Response extends Jsonable { + + @JsonSerialize + record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { + + } + + @JsonSerialize + record ArtifactAlreadyRegistered( + @JsonProperty String artifactName, + @JsonProperty String groupId + ) implements Response { + + } + + @JsonSerialize + record GroupMissing(@JsonProperty("groupId") String s) implements Response { + + } + + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java new file mode 100644 index 00000000..4c3de2a0 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java @@ -0,0 +1,60 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 java.util.List; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GetArtifactsResponse.GroupMissing.class, name = "UnknownGroup"), + @JsonSubTypes.Type(value = GetArtifactsResponse.ArtifactsAvailable.class, name = "Artifacts"), +}) +public sealed interface GetArtifactsResponse { + + @JsonSerialize + record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { + + @JsonCreator + public GroupMissing { + } + + } + + @JsonSerialize + record ArtifactsAvailable(@JsonProperty List artifactIds) + implements GetArtifactsResponse { + + @JsonCreator + public ArtifactsAvailable { + } + + } +} 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 new file mode 100644 index 00000000..040345fb --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java @@ -0,0 +1,55 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.Group; + +public final class GroupRegistration { + + @JsonDeserialize + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website + ) { + + @JsonCreator + public RegisterGroupRequest { } + + } + + public interface Response { + + record GroupAlreadyRegistered(String groupNameRequested) implements Response { + } + + record GroupRegistered(Group group) implements Response { + + } + } +} diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java new file mode 100644 index 00000000..a973e23f --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java @@ -0,0 +1,61 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.Group; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), + @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") +}) +public sealed interface GroupResponse extends Jsonable { + + @JsonSerialize + record Missing(@JsonProperty String groupId) implements GroupResponse { + @JsonCreator + public Missing(final String groupId) { + this.groupId = groupId; + } + + } + + @JsonSerialize + record Available(@JsonProperty Group group) implements GroupResponse { + + @JsonCreator + public Available(final Group group) { + this.group = group; + } + + } + +} 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 new file mode 100644 index 00000000..78416658 --- /dev/null +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java @@ -0,0 +1,48 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifact.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 io.vavr.collection.List; +import org.spongepowered.downloads.artifact.api.Group; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") +}) +public interface GroupsResponse { + + @JsonSerialize + record Available(@JsonProperty List groups) + implements GroupsResponse { + @JsonCreator + public Available { + } + } +} diff --git a/artifacts/build.gradle.kts b/artifacts/build.gradle.kts new file mode 100644 index 00000000..8c40ab6c --- /dev/null +++ b/artifacts/build.gradle.kts @@ -0,0 +1,18 @@ + + +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project + +subprojects { + dependencies { + implementation(project(":akka")) + } +} +dependencies { + +} + + + diff --git a/artifacts/events/build.gradle.kts b/artifacts/events/build.gradle.kts new file mode 100644 index 00000000..61ccd4eb --- /dev/null +++ b/artifacts/events/build.gradle.kts @@ -0,0 +1,14 @@ + + +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project + +dependencies { + api(project(":artifacts:api")) + +} + + + diff --git a/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java new file mode 100644 index 00000000..f2fe681c --- /dev/null +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/ArtifactEvent.java @@ -0,0 +1,74 @@ +package org.spongepowered.downloads.artifacts.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +public sealed interface ArtifactEvent { + + ArtifactCoordinates coordinates(); + + default String partitionKey() { + return this.coordinates().asMavenString(); + } + + @JsonTypeName("registered") + @JsonDeserialize + final record ArtifactRegistered( + ArtifactCoordinates coordinates + ) implements ArtifactEvent { + + @JsonCreator + public ArtifactRegistered { + } + } + + @JsonTypeName("git-repository") + @JsonDeserialize + final record GitRepositoryAssociated( + ArtifactCoordinates coordinates, + String repository + ) implements ArtifactEvent { + + @JsonCreator + public GitRepositoryAssociated { + } + } + + @JsonTypeName("website") + @JsonDeserialize + final record WebsiteUpdated( + ArtifactCoordinates coordinates, + String url + ) implements ArtifactEvent { + + @JsonCreator + public WebsiteUpdated { + } + } + + @JsonTypeName("issues") + @JsonDeserialize + final record IssuesUpdated( + ArtifactCoordinates coordinates, + String url + ) implements ArtifactEvent { + + @JsonCreator + public IssuesUpdated { + } + } + + @JsonTypeName("displayName") + @JsonDeserialize + final record DisplayNameUpdated( + ArtifactCoordinates coordinates, + String displayName + ) implements ArtifactEvent { + + @JsonCreator + public DisplayNameUpdated { + } + } +} 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 new file mode 100644 index 00000000..fd838b95 --- /dev/null +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/GroupUpdate.java @@ -0,0 +1,73 @@ +/* + * 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.events; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +import java.io.Serial; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(GroupUpdate.GroupRegistered.class), + @JsonSubTypes.Type(GroupUpdate.ArtifactRegistered.class), +}) +public interface GroupUpdate { + + String groupId(); + + @JsonTypeName("group-registered") + @JsonDeserialize + record GroupRegistered(String groupId, String name, String website) + implements GroupUpdate { + + @JsonCreator + public GroupRegistered { + } + + } + + @JsonTypeName("artifact-registered") + @JsonDeserialize + final record ArtifactRegistered(ArtifactCoordinates coordinates) implements GroupUpdate { + + @Serial private static final long serialVersionUID = 6319289932327553919L; + + @JsonCreator + public ArtifactRegistered { + } + + + @Override + public String groupId() { + return this.coordinates.groupId(); + } + } + +} diff --git a/artifacts/gradle.properties b/artifacts/gradle.properties new file mode 100644 index 00000000..a9d280a0 --- /dev/null +++ b/artifacts/gradle.properties @@ -0,0 +1 @@ +micronautVersion=3.8.2 diff --git a/artifacts/gradle/wrapper/gradle-wrapper.jar b/artifacts/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + 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/artifacts/gradlew.bat b/artifacts/gradlew.bat new file mode 100644 index 00000000..f127cfd4 --- /dev/null +++ b/artifacts/gradlew.bat @@ -0,0 +1,91 @@ +@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/artifacts/micronaut-cli.yml b/artifacts/micronaut-cli.yml new file mode 100644 index 00000000..dc960e1f --- /dev/null +++ b/artifacts/micronaut-cli.yml @@ -0,0 +1,6 @@ +applicationType: default +defaultPackage: org.spongepowered.downloads.artifacts +testFramework: junit +sourceLanguage: java +buildTool: gradle +features: [annotation-api, app-name, cache-caffeine, data, data-r2dbc, discovery-kubernetes, github-workflow-ci, graalvm, gradle, h2, http-client, java, java-application, junit, junit-params, kafka, logback, micronaut-aot, micronaut-build, netty-server, openapi, r2dbc, readme, security-annotations, security-jwt, security-ldap, serialization-jackson, shade, test-resources, toml, toml-build] diff --git a/artifacts/server/build.gradle.kts b/artifacts/server/build.gradle.kts new file mode 100644 index 00000000..21f64ea2 --- /dev/null +++ b/artifacts/server/build.gradle.kts @@ -0,0 +1,79 @@ + + +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project +val vavr: String by project + + +tasks { + dockerBuild { + images.add("${project.name}:${project.version}") + } + dockerBuildNative { + images.add("${project.name}:${project.version}") + + } +} +graalvmNative.toolchainDetection.set(false) +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("systemofadownload.*") + } + testResources { + additionalModules.add("r2dbc-postgresql") + } +} +graalvmNative { + binaries { + named("main") { + imageName.set("mn-graalvm-application") + buildArgs("--verboase") + } + } +} + +dependencies { + implementation(project(":artifacts:api")) + implementation("io.vavr:vavr:${vavr}") + annotationProcessor("io.micronaut.data:micronaut-data-processor") + annotationProcessor("io.micronaut:micronaut-http-validation") + annotationProcessor("io.micronaut.openapi:micronaut-openapi") + annotationProcessor("io.micronaut.security:micronaut-security-annotations") + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + implementation("com.ongres.scram:client:2.1") + implementation("io.micronaut:micronaut-http-client") + implementation("io.micronaut:micronaut-jackson-databind") + implementation("io.micronaut.data:micronaut-data-r2dbc") + implementation("io.micronaut.liquibase:micronaut-liquibase") + implementation("io.micronaut.reactor:micronaut-reactor") + implementation("io.micronaut.reactor:micronaut-reactor-http-client") + implementation("io.micronaut.security:micronaut-security-ldap") + implementation("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut.toml:micronaut-toml") + implementation("io.micronaut.xml:micronaut-jackson-xml") + implementation("io.swagger.core.v3:swagger-annotations") + implementation("io.vertx:vertx-pg-client") + implementation("jakarta.annotation:jakarta.annotation-api") + implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) + implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") + implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") + implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") + + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.postgresql:postgresql") + runtimeOnly("org.postgresql:r2dbc-postgresql") + compileOnly("org.graalvm.nativeimage:svm") + + implementation("io.micronaut:micronaut-validation") +} 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 new file mode 100644 index 00000000..96a9adb2 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java @@ -0,0 +1,48 @@ +package org.spongepowered.downloads.artifacts.server; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.SpawnProtocol; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.runtime.Micronaut; +import io.micronaut.runtime.event.annotation.EventListener; +import io.micronaut.runtime.server.event.ServerStartupEvent; +import io.swagger.v3.oas.annotations.*; +import io.swagger.v3.oas.annotations.info.*; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.spongepowered.downloads.artifacts.server.global.GlobalManager; + +@OpenAPIDefinition( + info = @Info( + title = "artifacts", + version = "0.0" + ) +) +@Singleton +@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) { + Micronaut.run(Application.class, args); + } + + @EventListener + public void onApplicationEvent(final ServerStartupEvent event) { + + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java new file mode 100644 index 00000000..9f014a26 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java @@ -0,0 +1,213 @@ +/* + * 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.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 com.lightbend.lagom.javadsl.api.transport.NotFound; +import com.lightbend.lagom.javadsl.persistence.AkkaTaggerAdapter; +import io.vavr.control.Either; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.details.state.DetailsState; +import org.spongepowered.downloads.artifact.details.state.EmptyState; +import org.spongepowered.downloads.artifact.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 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 final Function> tagger; + + private ArtifactDetailsEntity( + ActorContext ctx, + final EntityContext context, + String entityId, PersistenceId persistenceId + ) { + super(persistenceId); + this.artifactId = entityId; + this.ctx = ctx; + this.tagger = AkkaTaggerAdapter.fromLagom(context, DetailsEvent.TAG); + } + + 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( + 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( + 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( + 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( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + ) + ) + ) + ); + + return builder.build(); + } + + @Override + public Set tagsFor(final DetailsEvent detailsEvent) { + return this.tagger.apply(detailsEvent); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java new file mode 100644 index 00000000..bcb8d442 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java @@ -0,0 +1,114 @@ +/* + * 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.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.micronaut.http.HttpResponse; +import io.vavr.control.Either; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; + +import java.net.URL; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DetailsCommand.RegisterArtifact.class, + name = "register"), + @JsonSubTypes.Type(value = DetailsCommand.UpdateWebsite.class, + name = "website"), + @JsonSubTypes.Type(value = DetailsCommand.UpdateGitRepository.class, + name = "git-repository" + ), + @JsonSubTypes.Type(value = DetailsCommand.UpdateIssues.class, + name = "issues"), + @JsonSubTypes.Type(value = DetailsCommand.UpdateDisplayName.class, + name = "display-name") +}) +public interface DetailsCommand { + + @JsonDeserialize + final record RegisterArtifact(ArtifactCoordinates coordinates, + String displayName, ActorRef replyTo) + implements DetailsCommand { + + @JsonCreator + public RegisterArtifact { + } + } + + @JsonDeserialize + final record UpdateWebsite( + ArtifactCoordinates coordinates, + URL website, + ActorRef> replyTo + ) implements DetailsCommand { + + @JsonCreator + public UpdateWebsite { + } + } + + @JsonDeserialize + final record UpdateDisplayName( + ArtifactCoordinates coordinates, + String displayName, + ActorRef> replyTo + ) implements DetailsCommand { + + @JsonCreator + public UpdateDisplayName { + } + } + + @JsonDeserialize + final record UpdateGitRepository( + ArtifactCoordinates coordinates, + String gitRemote, + ActorRef> replyTo + ) implements DetailsCommand { + + @JsonCreator + public UpdateGitRepository { + } + } + + @JsonDeserialize + final record UpdateIssues( + ArtifactCoordinates coords, + URL validUrl, + ActorRef> replyTo + ) implements DetailsCommand { + + @JsonCreator + public UpdateIssues { + } + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java new file mode 100644 index 00000000..80886e24 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java @@ -0,0 +1,109 @@ +/* + * 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.details; + +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 com.lightbend.lagom.javadsl.persistence.AggregateEvent; +import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; +import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DetailsEvent.ArtifactRegistered.class, name = "registered"), + @JsonSubTypes.Type(value = DetailsEvent.ArtifactDetailsUpdated.class, name = "details"), + @JsonSubTypes.Type(value = DetailsEvent.ArtifactIssuesUpdated.class, name = "issues"), + @JsonSubTypes.Type(value = DetailsEvent.ArtifactGitRepositoryUpdated.class, name = "git-repo"), + @JsonSubTypes.Type(value = DetailsEvent.ArtifactWebsiteUpdated.class, name = "website"), +}) +public interface DetailsEvent extends AggregateEvent, Jsonable { + + AggregateEventShards TAG = AggregateEventTag.sharded(DetailsEvent.class, 3); + + @Override + default AggregateEventTagger aggregateTag() { + return TAG; + } + + @JsonDeserialize + record ArtifactRegistered( + ArtifactCoordinates coordinates + ) implements DetailsEvent { + @JsonCreator + public ArtifactRegistered { + } + } + + @JsonDeserialize + record ArtifactDetailsUpdated( + ArtifactCoordinates coordinates, + String displayName + ) implements DetailsEvent { + + @JsonCreator + public ArtifactDetailsUpdated { + } + } + + @JsonDeserialize + record ArtifactIssuesUpdated( + ArtifactCoordinates coordinates, + String url + ) implements DetailsEvent { + + @JsonCreator + public ArtifactIssuesUpdated { + } + } + + @JsonDeserialize + record ArtifactGitRepositoryUpdated( + ArtifactCoordinates coordinates, + String gitRepo + ) implements DetailsEvent { + + @JsonCreator + public ArtifactGitRepositoryUpdated { + } + } + + @JsonDeserialize + record ArtifactWebsiteUpdated( + ArtifactCoordinates coordinates, + String url + ) implements DetailsEvent { + + @JsonCreator + public ArtifactWebsiteUpdated { + } + } + + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java new file mode 100644 index 00000000..e851dbca --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java @@ -0,0 +1,136 @@ +package org.spongepowered.downloads.artifacts.server.details; + +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.http.HttpResponse; +import io.vavr.control.Either; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.lib.Ref; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; + +import java.net.URL; +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletionStage; + +@Singleton +public class DetailsManager { + private final ClusterSharding clusterSharding; + private final Duration askTimeout = Duration.ofHours(5); + + @Inject + public DetailsManager(final ClusterSharding clusterSharding) { + this.clusterSharding = clusterSharding; + this.clusterSharding.init( + Entity.of( + ArtifactDetailsEntity.ENTITY_TYPE_KEY, + context -> ArtifactDetailsEntity.create( + context, + context.getEntityId(), + PersistenceId.of(context.getEntityTypeKey().name(), context.getEntityId()) + ) + ) + ); + } + + public CompletionStage UpdateWebsite( + ArtifactCoordinates coords, ArtifactDetails.Update.Website w + ) { + + final Either validate = w.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var validUrl = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateWebsite(coords, validUrl, r), this.askTimeout) + .thenApply(Either::get); + } + + + public CompletionStage UpdateDisplayName( + ArtifactCoordinates coords, ArtifactDetails.Update.DisplayName d + ) { + final Either validate = d.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var displayName = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateDisplayName(coords, displayName, r), + this.askTimeout + ) + .thenApply(Either::get); + } + + public CompletionStage UpdateGitRepository( + final ArtifactCoordinates coords, ArtifactDetails.Update.GitRepository gr + ) { + final Either validate = gr.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final Collection refs; + try { + refs = Git.lsRemoteRepository() + .setRemote(gr.gitRepo()) + .call(); + } catch (InvalidRemoteException e) { + throw new BadRequest(String.format("Invalid remote: %s", gr.gitRepo())); + } catch (GitAPIException e) { + throw new BadRequest(String.format("Error resolving repository '%s'", gr.gitRepo())); + } + if (refs.isEmpty()) { + throw new BadRequest(String.format("Remote repository '%s' has no refs", gr.gitRepo())); + } + + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateGitRepository(coords, gr.gitRepo(), r), this.askTimeout) + .thenApply(Either::get); + } + + public CompletionStage UpdateIssues( + ArtifactCoordinates coords, ArtifactDetails.Update.Issues i + ) { + final Either validate = i.validate(); + if (validate.isLeft()) { + throw validate.getLeft(); + } + final var validUrl = validate.get(); + return this.getDetailsEntity(coords) + .>ask( + r -> new DetailsCommand.UpdateIssues(coords, validUrl, r), this.askTimeout).thenApply( + Either::get); + } + + private EntityRef getDetailsEntity(final ArtifactCoordinates coordinates) { + return this.getDetailsEntity(coordinates.groupId(), coordinates.artifactId()); + } + + private EntityRef getDetailsEntity(final String groupId, final String artifactId) { + return this.clusterSharding.entityRefFor(ArtifactDetailsEntity.ENTITY_TYPE_KEY, groupId + ":" + artifactId); + } + + public CompletionStage registerArtifact( + ArtifactRegistration.Response.ArtifactRegistered registered, + String displayName + ) { + return this.getDetailsEntity(registered.coordinates()) + .ask( + replyTo -> new DetailsCommand.RegisterArtifact( + registered.coordinates(), displayName, replyTo), this.askTimeout) + .thenApply(notUsed -> registered); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java new file mode 100644 index 00000000..5695e90f --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java @@ -0,0 +1,43 @@ +/* + * 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.details.state; + +import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +public interface DetailsState extends Jsonable { + ArtifactCoordinates coordinates(); + + String displayName(); + + String website(); + + String gitRepository(); + + String issues(); + + boolean isEmpty(); + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java new file mode 100644 index 00000000..2302278a --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/EmptyState.java @@ -0,0 +1,68 @@ +/* + * 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.details.state; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +@JsonDeserialize +public final class EmptyState implements DetailsState { + private static final ArtifactCoordinates empty = new ArtifactCoordinates("", ""); + + @JsonCreator + public EmptyState() { + } + + @Override + public ArtifactCoordinates coordinates() { + return empty; + } + + @Override + public String displayName() { + return ""; + } + + @Override + public String website() { + return ""; + } + + @Override + public String gitRepository() { + return ""; + } + + @Override + public String issues() { + return ""; + } + + @Override + public boolean isEmpty() { + return true; + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java new file mode 100644 index 00000000..e120427d --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java @@ -0,0 +1,85 @@ +/* + * 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.details.state; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.serialization.CompressedJsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.details.DetailsEvent; + +@JsonDeserialize +public record PopulatedState(ArtifactCoordinates coordinates, + String displayName, String website, String gitRepository, + String issues) implements DetailsState, CompressedJsonable { + + @JsonCreator + public PopulatedState { + } + + public boolean isEmpty() { + return this.coordinates.artifactId().isBlank() && this.coordinates.groupId().isBlank(); + } + + public DetailsState withDisplayName(DetailsEvent.ArtifactDetailsUpdated event) { + return new PopulatedState( + this.coordinates, + event.displayName(), + this.website, + this.gitRepository, + this.issues + ); + } + + public DetailsState withGitRepo(DetailsEvent.ArtifactGitRepositoryUpdated e) { + return new PopulatedState( + this.coordinates, + this.displayName, + this.website, + e.gitRepo(), + this.issues + ); + } + + public DetailsState withIssues(DetailsEvent.ArtifactIssuesUpdated e) { + return new PopulatedState( + this.coordinates, + this.displayName, + this.website, + this.gitRepository, + e.url() + ); + } + + public DetailsState withWebsite(DetailsEvent.ArtifactWebsiteUpdated e) { + return new PopulatedState( + this.coordinates, + this.displayName, + e.url(), + this.gitRepository, + this.issues + ); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java new file mode 100644 index 00000000..1fcaf252 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java @@ -0,0 +1,62 @@ +/* + * 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.global; + +import akka.Done; +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 com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "Get", + value = GlobalCommand.GetGroups.class) +}) +public interface GlobalCommand extends Jsonable { + + @JsonDeserialize + record GetGroups(ActorRef replyTo) + implements GlobalCommand { + + @JsonCreator + public GetGroups { + } + } + + @JsonDeserialize + record RegisterGroup( + ActorRef replyTo, Group group) implements GlobalCommand { + + @JsonCreator + public RegisterGroup { + } + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java new file mode 100644 index 00000000..d9162e8f --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java @@ -0,0 +1,53 @@ +/* + * This file is part of SystemOfADownload, licensed under the MIT License (MIT). + * + * Copyright (c) SpongePowered + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.spongepowered.downloads.artifacts.server.global; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.javadsl.persistence.AggregateEvent; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; +import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.Group; + +public interface GlobalEvent extends AggregateEvent, Jsonable { + + AggregateEventTag TAG = AggregateEventTag.of(GlobalEvent.class); + + @Override + default AggregateEventTagger aggregateTag() { + return TAG; + } + + @JsonDeserialize + final class GroupRegistered implements GlobalEvent { + public final Group group; + + @JsonCreator + public GroupRegistered(Group group) { + this.group = group; + } + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java new file mode 100644 index 00000000..907ad2b0 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java @@ -0,0 +1,54 @@ +package org.spongepowered.downloads.artifacts.server.global; + +import akka.Done; +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 jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; + +import java.time.Duration; +import java.util.concurrent.CompletionStage; + +@Singleton +public final class GlobalManager { + private final Duration askTimeout = Duration.ofHours(5); + + private final ClusterSharding clusterSharding; + + @Inject + public GlobalManager(final ClusterSharding clusterSharding) { + this.clusterSharding = clusterSharding; + this.clusterSharding.init( + Entity.of( + GlobalRegistration.ENTITY_TYPE_KEY, + ctx -> GlobalRegistration.create( + ctx.getEntityId(), + PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()) + ) + ) + ); + } + + public CompletionStage registerGroup( + GroupRegistration.Response.GroupRegistered registered + ) { + final Group group = registered.group(); + return this.getGlobalEntity() + .ask(replyTo -> new GlobalCommand.RegisterGroup(replyTo, group), this.askTimeout) + .thenApply(notUsed -> registered); + } + + + private EntityRef getGlobalEntity() { + return this.clusterSharding.entityRefFor(GlobalRegistration.ENTITY_TYPE_KEY, "global"); + } + + public CompletionStage getGroups() { + return this.getGlobalEntity().ask(GlobalCommand.GetGroups::new, this.askTimeout); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java new file mode 100644 index 00000000..52d20c76 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java @@ -0,0 +1,99 @@ +/* + * 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.global; + +import akka.Done; +import akka.actor.typed.Behavior; +import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; +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 akka.persistence.typed.javadsl.ReplyEffect; +import io.vavr.collection.List; +import org.spongepowered.downloads.artifact.api.query.GroupsResponse; + +public class GlobalRegistration + extends EventSourcedBehaviorWithEnforcedReplies { + + public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( + GlobalCommand.class, "GlobalEntity"); + private final String groupId; + private final ActorContext ctx; + + private GlobalRegistration(ActorContext ctx, String entityId, PersistenceId persistenceId) { + super(persistenceId); + this.ctx = ctx; + this.groupId = entityId; + } + + public static Behavior create(String entityId, PersistenceId persistenceId) { + return Behaviors.setup(ctx -> new GlobalRegistration(ctx, entityId, persistenceId)); + } + + @Override + public GlobalState emptyState() { + return new GlobalState(List.empty()); + } + + @Override + public EventHandler eventHandler() { + final var builder = this.newEventHandlerBuilder(); + builder.forAnyState() + .onEvent( + GlobalEvent.GroupRegistered.class, + (state, event) -> new GlobalState(state.groups().append(event.group)) + ); + return builder.build(); + } + + @Override + public CommandHandlerWithReply commandHandler() { + final var builder = this.newCommandHandlerWithReplyBuilder(); + builder.forAnyState() + .onCommand( + GlobalCommand.GetGroups.class, + (state, cmd) -> this.Effect().reply(cmd.replyTo(), new GroupsResponse.Available(state.groups())) + ) + .onCommand( + GlobalCommand.RegisterGroup.class, + this::handleRegisterGroup + ); + return builder.build(); + } + + private ReplyEffect handleRegisterGroup( + GlobalState state, GlobalCommand.RegisterGroup cmd + ) { + if (!state.groups().contains(cmd.group())) { + return this.Effect().persist(new GlobalEvent.GroupRegistered(cmd.group())) + .thenReply(cmd.replyTo(), (s) -> Done.done()); + } + return this.Effect().reply(cmd.replyTo(), Done.done()); + } + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java new file mode 100644 index 00000000..9a26c915 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java @@ -0,0 +1,38 @@ +/* + * 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.global; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.collection.List; +import org.spongepowered.downloads.artifact.api.Group; + +@JsonDeserialize +public record GlobalState(List groups) { + + @JsonCreator + public GlobalState { + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java new file mode 100644 index 00000000..08c03a79 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java @@ -0,0 +1,63 @@ +/* + * 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.groups; + +import akka.actor.typed.ActorRef; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; + +public sealed interface GroupCommand { + record GetGroup( + String groupId, + ActorRef replyTo + ) implements GroupCommand { + } + + record GetArtifacts( + String groupId, + ActorRef replyTo + ) implements GroupCommand { + + } + + record RegisterArtifact( + String artifact, + ActorRef replyTo + ) implements GroupCommand { + + } + + record RegisterGroup( + String mavenCoordinates, + String name, + String website, + ActorRef replyTo + ) implements GroupCommand { + + } + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java new file mode 100644 index 00000000..d2fa9f83 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java @@ -0,0 +1,206 @@ +/* + * 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.groups; + +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.control.Try; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; +import org.spongepowered.downloads.artifact.api.Group; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +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.artifacts.server.groups.state.EmptyState; +import org.spongepowered.downloads.artifacts.server.groups.state.GroupState; +import org.spongepowered.downloads.artifacts.server.groups.state.PopulatedState; + +import java.net.URL; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GroupEntity + extends EventSourcedBehaviorWithEnforcedReplies { + + public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create(GroupCommand.class, "GroupEntity"); + private final String groupId; + private final Function> tagger; + + 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(); + this.tagger = AkkaTaggerAdapter.fromLagom(context, GroupEvent.TAG); + + } + + 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( + GroupEvent.GroupRegistered.class, + this::handleRegistration + ); + builder.forStateType(PopulatedState.class) + .onEvent(GroupEvent.ArtifactRegistered.class, this::handleArtifactRegistration); + + return builder.build(); + } + + private GroupState handleRegistration( + final GroupState state, final GroupEvent.GroupRegistered event + ) { + return new PopulatedState(event.groupId, event.name, event.website, Set.of()); + } + + private GroupState handleArtifactRegistration( + final PopulatedState state, final GroupEvent.ArtifactRegistered event + ) { + final var add = Stream.concat( + state.artifacts().stream(), + Stream.of(event.artifact()) + ) + .collect(Collectors.toUnmodifiableSet()); + 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 ArtifactRegistration.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); + return builder.build(); + } + + @Override + public RetentionCriteria retentionCriteria() { + return RetentionCriteria.snapshotEvery(5, 2); + } + + @Override + public Set tagsFor(final GroupEvent groupEvent) { + return this.tagger.apply(groupEvent); + } + + private ReplyEffect respondToRegisterGroup( + final GroupState state, + final GroupCommand.RegisterGroup cmd + ) { + return this.Effect() + .persist(new GroupEvent.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 ArtifactRegistration.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 GroupEvent.ArtifactRegistered(state.groupCoordinates(), cmd.artifact())) + .thenReply(cmd.replyTo(), (s) -> new ArtifactRegistration.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(() -> new URL(website)) + .mapTry(url -> { + final Group group = new Group(state.groupCoordinates(), state.name(), website); + return new GroupResponse.Available(group); + }) + .getOrElseGet(throwable -> 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().stream().toList())); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java new file mode 100644 index 00000000..01fdb996 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java @@ -0,0 +1,124 @@ +/* + * 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.groups; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.javadsl.persistence.AggregateEvent; +import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; +import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; +import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + +import java.io.Serial; +import java.util.Objects; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(GroupEvent.GroupRegistered.class), + @JsonSubTypes.Type(GroupEvent.ArtifactRegistered.class), +}) +public interface GroupEvent extends AggregateEvent, Jsonable { + + AggregateEventShards TAG = AggregateEventTag.sharded(GroupEvent.class, 10); + + @Override + default AggregateEventTagger aggregateTag() { + return TAG; + } + + String groupId(); + + @JsonTypeName("group-registered") + @JsonDeserialize + final class GroupRegistered implements GroupEvent { + @Serial private static final long serialVersionUID = 0L; + + public final String groupId; + public final String name; + public final String website; + + @JsonCreator + public GroupRegistered(final String groupId, final String name, final String website) { + this.groupId = groupId; + this.name = name; + this.website = website; + } + + @Override + public String groupId() { + return this.groupId; + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + final var that = (GroupRegistered) obj; + return Objects.equals(this.groupId, that.groupId) && + Objects.equals(this.name, that.name) && + Objects.equals(this.website, that.website); + } + + @Override + public int hashCode() { + return Objects.hash(this.groupId, this.name, this.website); + } + + @Override + public String toString() { + return "GroupRegistered[" + + "groupId=" + this.groupId + ", " + + "name=" + this.name + ", " + + "website=" + this.website + ']'; + } + + } + + @JsonTypeName("artifact-registered") + @JsonDeserialize + record ArtifactRegistered( + String groupId, + String artifact + ) implements GroupEvent { + + public ArtifactCoordinates coordinates() { + return new ArtifactCoordinates(this.groupId, this.artifact); + } + + @JsonCreator + public ArtifactRegistered { + } + } + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java new file mode 100644 index 00000000..7783d3cf --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java @@ -0,0 +1,100 @@ +package org.spongepowered.downloads.artifacts.server.groups; + +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.cluster.sharding.typed.javadsl.EntityRef; +import com.lightbend.lagom.javadsl.api.transport.NotFound; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; +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.artifacts.server.details.DetailsManager; +import org.spongepowered.downloads.artifacts.server.global.GlobalManager; + +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Singleton +public final class GroupManager { + private final ClusterSharding clusterSharding; + private final GlobalManager global; + private final Duration askTimeout = Duration.ofHours(5); + private final DetailsManager details; + + @Inject + public GroupManager(ClusterSharding clusterSharding, final DetailsManager details, final GlobalManager global) { + this.clusterSharding = clusterSharding; + this.global = global; + this.details = details; + this.clusterSharding.init( + Entity.of( + GroupEntity.ENTITY_TYPE_KEY, + GroupEntity::create + ) + ); + } + + public CompletionStage registerGroup( + GroupRegistration.RegisterGroupRequest registration + ) { + final String mavenCoordinates = registration.groupCoordinates(); + final String name = registration.name(); + final String website = registration.website(); + return this.groupEntity(registration.groupCoordinates().toLowerCase(Locale.ROOT)) + .ask( + replyTo -> new GroupCommand.RegisterGroup(mavenCoordinates, name, website, replyTo), + this.askTimeout + ).thenCompose(response -> { + if (!(response instanceof GroupRegistration.Response.GroupRegistered registered)) { + return CompletableFuture.completedFuture(response); + } + return this.global.registerGroup(registered); + + }); + } + + + private EntityRef groupEntity(final String groupId) { + return this.clusterSharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, groupId.toLowerCase(Locale.ROOT)); + } + + public CompletionStage registerArtifact( + ArtifactRegistration.RegisterArtifact reg, String groupId + ) { + final var sanitizedGroupId = groupId.toLowerCase(Locale.ROOT); + return this.groupEntity(sanitizedGroupId) + .ask( + replyTo -> new GroupCommand.RegisterArtifact(reg.artifactId(), replyTo), this.askTimeout) + .thenCompose(response -> switch (response) { + case ArtifactRegistration.Response.GroupMissing missing -> + throw new NotFound(String.format("group %s does not exist", missing.s())); + + case ArtifactRegistration.Response.ArtifactRegistered registered -> + this.details.registerArtifact(registered, reg.displayName()); + + default -> CompletableFuture.completedFuture(response); + }); + } + + public CompletionStage getArtifacts(String groupId) { + return this.groupEntity(groupId) + .ask(replyTo -> new GroupCommand.GetArtifacts(groupId, replyTo), this.askTimeout) + .thenApply(response -> switch (response) { + case GetArtifactsResponse.GroupMissing m -> throw new NotFound(String.format("group '%s' not found", m.groupRequested())); + case GetArtifactsResponse.ArtifactsAvailable a -> a; + }); + } + + public CompletionStage get(String groupId) { + return this.groupEntity(groupId.toLowerCase(Locale.ROOT)) + .ask(replyTo -> new GroupCommand.GetGroup(groupId, replyTo), this.askTimeout) + .thenApply(response -> switch (response) { + case GroupResponse.Missing m -> throw new NotFound(String.format("group '%s' not found", m.groupId())); + case GroupResponse.Available a -> a; + }); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java new file mode 100644 index 00000000..98ebb288 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java @@ -0,0 +1,65 @@ +package org.spongepowered.downloads.artifacts.server.groups; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.SpawnProtocol; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.cluster.sharding.typed.javadsl.EntityRef; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import jakarta.inject.Inject; +import org.spongepowered.downloads.artifact.api.query.GroupRegistration; +import org.spongepowered.downloads.artifact.api.query.GroupResponse; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Controller("/groups") +public class GroupsQueryController { + + private final ActorSystem system; + private final ClusterSharding sharding; + + @Inject + public GroupsQueryController( + final ActorSystem system, + ClusterSharding sharding + ) { + this.system = system; + this.sharding = sharding; + this.sharding.init( + Entity.of( + GroupEntity.ENTITY_TYPE_KEY, + GroupEntity::create + ) + ); + + } + + @Post("/") + public CompletableFuture> registerGroup( + @Body GroupRegistration.RegisterGroupRequest req + ) { + final var ref = this.sharding.entityRefFor( + GroupEntity.ENTITY_TYPE_KEY, + req.groupCoordinates() + ); + final var resp = ref.ask + ( + replyTo -> new GroupCommand.RegisterGroup( + req.groupCoordinates(), + req.name(), + req.website(), + replyTo + ), + Duration.ofSeconds(10) + ); + return resp + .>thenApply(HttpResponse::created) + .toCompletableFuture(); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java new file mode 100644 index 00000000..e6b3de5a --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java @@ -0,0 +1,62 @@ +/* + * 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.groups.state; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.Group; + +@JsonDeserialize +public record EmptyState() implements GroupState { + + @JsonCreator + public EmptyState { + } + + @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/groups/state/GroupState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java new file mode 100644 index 00000000..8b133996 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java @@ -0,0 +1,47 @@ +/* + * 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.groups.state; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.Group; + +@JsonDeserialize +@JsonSubTypes({ + @JsonSubTypes.Type(value = PopulatedState.class, name = "populated"), + @JsonSubTypes.Type(value = EmptyState.class, name = "empty") +}) +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/groups/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java new file mode 100644 index 00000000..56264f25 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java @@ -0,0 +1,51 @@ +/* + * 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.groups.state; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.artifact.api.Group; + +import java.util.Set; + +@JsonDeserialize +public record PopulatedState( + String groupCoordinates, + String name, + String website, + Set artifacts +) implements GroupState { + @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/query/ArtifactsQuery.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java new file mode 100644 index 00000000..fb46c84f --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java @@ -0,0 +1,12 @@ +package org.spongepowered.downloads.artifacts.server.query; + +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; + +@Controller("/groups/{groupID}/artifacts") +public class ArtifactsQuery { + + @Get("") + + +} diff --git a/artifacts/server/src/main/resources/application.toml b/artifacts/server/src/main/resources/application.toml new file mode 100644 index 00000000..250d1b57 --- /dev/null +++ b/artifacts/server/src/main/resources/application.toml @@ -0,0 +1,10 @@ +micronaut.application.name = 'artifacts' +netty.default.allocator.max-order = 3 + +[r2dbc.datasources.default] +schema-generate = 'CREATE_DROP' +dialect = 'H2' + +[micronaut.security] +authentication = 'bearer' +token.jwt.signatures.secret.generator.secret = '${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}' diff --git a/artifacts/server/src/main/resources/bootstrap.toml b/artifacts/server/src/main/resources/bootstrap.toml new file mode 100644 index 00000000..82bc8221 --- /dev/null +++ b/artifacts/server/src/main/resources/bootstrap.toml @@ -0,0 +1,4 @@ + +[kubernetes.client.discovery] +mode = 'endpoint' +mode-configuration.endpoint.watch.enabled = true diff --git a/artifacts/server/src/main/resources/logback.xml b/artifacts/server/src/main/resources/logback.xml new file mode 100644 index 00000000..6010eb52 --- /dev/null +++ b/artifacts/server/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java new file mode 100644 index 00000000..4af14913 --- /dev/null +++ b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java @@ -0,0 +1,20 @@ +package org.spongepowered.downloads.artifacts.server; + +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.runtime.Micronaut; +import io.micronaut.runtime.server.event.ServerStartupEvent; +import io.swagger.v3.oas.annotations.*; +import io.swagger.v3.oas.annotations.info.*; + +@OpenAPIDefinition( + info = @Info( + title = "artifacts", + version = "0.0" + ) +) +public class Application implements ApplicationEventListener { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } +} diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java new file mode 100644 index 00000000..960d4ab5 --- /dev/null +++ b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java @@ -0,0 +1,9 @@ +package org.spongepowered.downloads.artifacts.server.query; + +import io.micronaut.http.annotation.Controller; + +@Controller("/groups/{groupID}/artifacts") +public class ArtifactsQuery { + + +} diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java new file mode 100644 index 00000000..6c5b218c --- /dev/null +++ b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java @@ -0,0 +1,28 @@ +package org.spongepowered.downloads.artifacts.server.query; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.SpawnProtocol; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.Post; +import jakarta.inject.Inject; + +@Controller("/groups") +public class GroupsQueryController { + + + @Inject + private ActorSystem system; + @Inject + private ClusterSharding sharding; + + @Post("/") + public HttpResponse registerGroup( + @Body GroupRegistration.RegisterGroupRequest req + ) { + return null; + } +} diff --git a/artifacts/src/main/resources/application.toml b/artifacts/src/main/resources/application.toml new file mode 100644 index 00000000..250d1b57 --- /dev/null +++ b/artifacts/src/main/resources/application.toml @@ -0,0 +1,10 @@ +micronaut.application.name = 'artifacts' +netty.default.allocator.max-order = 3 + +[r2dbc.datasources.default] +schema-generate = 'CREATE_DROP' +dialect = 'H2' + +[micronaut.security] +authentication = 'bearer' +token.jwt.signatures.secret.generator.secret = '${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}' diff --git a/artifacts/src/main/resources/bootstrap.toml b/artifacts/src/main/resources/bootstrap.toml new file mode 100644 index 00000000..82bc8221 --- /dev/null +++ b/artifacts/src/main/resources/bootstrap.toml @@ -0,0 +1,4 @@ + +[kubernetes.client.discovery] +mode = 'endpoint' +mode-configuration.endpoint.watch.enabled = true diff --git a/artifacts/src/main/resources/logback.xml b/artifacts/src/main/resources/logback.xml new file mode 100644 index 00000000..6010eb52 --- /dev/null +++ b/artifacts/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java b/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java new file mode 100644 index 00000000..13573ede --- /dev/null +++ b/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java @@ -0,0 +1,21 @@ +package org.spongepowered.downloads.artifacts; + +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import jakarta.inject.Inject; + +@MicronautTest(transactional = false) +class ArtifactsTest { + + @Inject + EmbeddedApplication application; + + @Test + void testItWorks() { + Assertions.assertTrue(application.isRunning()); + } + +} diff --git a/artifacts/worker/build.gradle.kts b/artifacts/worker/build.gradle.kts new file mode 100644 index 00000000..313f4efe --- /dev/null +++ b/artifacts/worker/build.gradle.kts @@ -0,0 +1,47 @@ + + + +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project + +dependencies { + implementation(project(":artifacts:api")) + annotationProcessor("io.micronaut.data:micronaut-data-processor") + annotationProcessor("io.micronaut:micronaut-http-validation") + annotationProcessor("io.micronaut.openapi:micronaut-openapi") + annotationProcessor("io.micronaut.security:micronaut-security-annotations") + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + implementation("com.ongres.scram:client:2.1") + implementation("io.micronaut:micronaut-http-client") + implementation("io.micronaut:micronaut-jackson-databind") + implementation("io.micronaut.data:micronaut-data-r2dbc") + implementation("io.micronaut.liquibase:micronaut-liquibase") + implementation("io.micronaut.reactor:micronaut-reactor") + implementation("io.micronaut.reactor:micronaut-reactor-http-client") + implementation("io.micronaut.security:micronaut-security-ldap") + implementation("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut.toml:micronaut-toml") + implementation("io.micronaut.xml:micronaut-jackson-xml") + implementation("io.swagger.core.v3:swagger-annotations") + implementation("io.vertx:vertx-pg-client") + implementation("jakarta.annotation:jakarta.annotation-api") + implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) + implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") + implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") + implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") + + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.postgresql:postgresql") + runtimeOnly("org.postgresql:r2dbc-postgresql") + compileOnly("org.graalvm.nativeimage:svm") + + implementation("io.micronaut:micronaut-validation") +} diff --git a/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/Application.java b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/Application.java new file mode 100644 index 00000000..873ebe25 --- /dev/null +++ b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/Application.java @@ -0,0 +1,33 @@ +package org.spongepowered.downloads.artifacts.worker; + + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.SpawnProtocol; +import akka.actor.typed.javadsl.AskPattern; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.runtime.Micronaut; +import io.micronaut.runtime.server.event.ServerStartupEvent; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.time.Duration; + +@Singleton +public class Application implements ApplicationEventListener { + + @Inject + private ActorSystem system; + + public static void main(final String[] args) { + Micronaut.run(Application.class, args); + } + + @Override + public void onApplicationEvent(final ServerStartupEvent event) { + AskPattern.ask(this.system, + response -> new SpawnProtocol.Spawn(), + Duration.ofSeconds(10), + this.system.scheduler() + ); + } +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..026df055 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,117 @@ + + +version = "0.1" +group = "systemofadownload" + +repositories { + mavenCentral() +} + +plugins { + `java-library` + `application` + id("com.github.johnrengelman.shadow") + id("io.micronaut.application") + id("io.micronaut.test-resources") +} +val akkaVersion: String by project +val scalaVersion: String by project +val akkaManagementVersion: String by project +val akkaProjection: String by project + +allprojects { + + apply(plugin = "java-library") + apply(plugin = "io.micronaut.application") + apply(plugin = "io.micronaut.test-resources") + apply(plugin = "com.github.johnrengelman.shadow") + + + java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + if (JavaVersion.current() < JavaVersion.VERSION_17) { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } + } + +} + +dependencies { + annotationProcessor("io.micronaut.data:micronaut-data-processor") + annotationProcessor("io.micronaut:micronaut-http-validation") + annotationProcessor("io.micronaut.openapi:micronaut-openapi") + annotationProcessor("io.micronaut.security:micronaut-security-annotations") + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + implementation("com.ongres.scram:client:2.1") + implementation("io.micronaut:micronaut-http-client") + implementation("io.micronaut:micronaut-jackson-databind") + implementation("io.micronaut.data:micronaut-data-r2dbc") + implementation("io.micronaut.liquibase:micronaut-liquibase") + implementation("io.micronaut.reactor:micronaut-reactor") + implementation("io.micronaut.reactor:micronaut-reactor-http-client") + implementation("io.micronaut.security:micronaut-security-ldap") + implementation("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut.toml:micronaut-toml") + implementation("io.micronaut.xml:micronaut-jackson-xml") + implementation("io.swagger.core.v3:swagger-annotations") + implementation("io.vertx:vertx-pg-client") + implementation("jakarta.annotation:jakarta.annotation-api") + implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) + implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") + implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}:${akkaProjection}") + implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") + implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") + implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") + + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.postgresql:postgresql") + runtimeOnly("org.postgresql:r2dbc-postgresql") + compileOnly("org.graalvm.nativeimage:svm") + + implementation("io.micronaut:micronaut-validation") + +} + + +application { + mainClass.set("systemofadownload.Application") +} +tasks { + dockerBuild { + images.add("${project.name}:${project.version}") + } + dockerBuildNative { + images.add("${project.name}:${project.version}") + + } +} +graalvmNative.toolchainDetection.set(false) +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("systemofadownload.*") + } + testResources { + additionalModules.add("r2dbc-postgresql") + } +} +graalvmNative { + binaries { + named("main") { + imageName.set("mn-graalvm-application") + buildArgs("--verboase") + } + } +} + + + diff --git a/build.sbt b/build.sbt index 0260d511..1cd077bb 100644 --- a/build.sbt +++ b/build.sbt @@ -298,25 +298,6 @@ def implSoadProjectWithPersistence(name: String, implFor: Project) = // region Project Definitions -lazy val `artifact-api` = apiSoadProject("artifact-api").settings( - //Maven Dependency for Version Parsing - libraryDependencies += "org.apache.maven" % "maven-artifact" % "3.8.6" -) -lazy val `artifact-impl` = implSoadProjectWithPersistence("artifact-impl", `artifact-api`).dependsOn( - //Inter module dependencies - `server-auth`, - `sonatype` -).settings( - libraryDependencies ++= Seq(jgit, playFilterHelpers) -) - -lazy val `artifact-query-api` = apiSoadProject("artifact-query-api").dependsOn( - //Inter module dependencies - `artifact-api` -) -lazy val `artifact-query-impl` = implSoadProjectWithPersistence("artifact-query-impl", `artifact-query-api`).settings( - libraryDependencies += playFilterHelpers -) lazy val `downloads-api` = soadProject("downloads-api").enablePlugins(DockerPlugin) .settings( libraryDependencies ++= Seq( @@ -362,122 +343,12 @@ lazy val `downloads-api` = soadProject("downloads-api").enablePlugins(DockerPlug ) ) -lazy val `versions-api` = apiSoadProject("versions-api").dependsOn( - //Module Dependencies - `artifact-api` -) - -lazy val `versions-impl` = implSoadProjectWithPersistence("versions-impl", `versions-api`).dependsOn( - //Other SystemOfADownload Common Implementation Dependencies - `server-auth` -).settings( - libraryDependencies ++= Seq( - jgit, - jgit_jsch, - playFilterHelpers, - hibernateTypes - ) -) - -lazy val `versions-query-api` = apiSoadProject("versions-query-api").dependsOn( - //Inter module dependencies - `artifact-api` -) -lazy val `versions-query-impl` = implSoadProjectWithPersistence("versions-query-impl", `versions-query-api`).settings( - libraryDependencies ++= Seq( - hibernateTypes, - playFilterHelpers - ) -) - -lazy val `version-synchronizer` = serverSoadProject("version-synchronizer").dependsOn( - //Modules we consume - `versions-api`, - `server-auth`, - `sonatype`, - `artifact-api` -).settings( - libraryDependencies ++= Seq( - //Lagom Dependencies - // We use kafka for all inter-service message forwarding - lagomJavadslKafkaBroker, - // Use persistence - lagomJavadslPersistenceJpa, - // Use Akka-Typed Streams - akkaStreamTyped, - //Database Dependencies - hibernate, - postgres, - //XML Deserialization - to interpret Maven's metadata.xml - jacksonDataformatXml, - // Jgit - jgit, - ) -) - -lazy val `sonatype` = soadProject("sonatype").settings( - libraryDependencies ++= Seq( - //Language Features - vavr, - //Jackson Serialization - "com.fasterxml.jackson.core" % "jackson-annotations" % "2.12.5", - "com.fasterxml.jackson.core" % "jackson-databind" % "2.12.5", - "com.fasterxml.jackson.core" % "jackson-core" % "2.12.5", - //Test Dependencies - junit, - jupiterInterface, - jacksonDataformatXml % Test, - vavrJackson % Test exclude("com.fasterxml.jackson.core", "jackson-databind") - ) -) - -lazy val `auth-api` = apiSoadProject("auth-api").settings( - //Auth Dependency - libraryDependencies ++= Seq( - lagomPac4j, - pac4jHttp, - pac4jJwt - ) -) -lazy val `auth-impl` = serverSoadProject("auth-impl").dependsOn( - //The service we're implementing - `auth-api`, - //Server Authentication Dependency - `server-auth` -).settings( - //LDAP dependency - libraryDependencies ++= Seq(lagomPac4jLdap, playFilterHelpers) -) - -lazy val `server-auth` = soadProject("server-auth").dependsOn(`auth-api`).settings( - libraryDependencies ++= Seq( - //Language Features - vavr, - //Lagom Server Dependency - lagomJavadslServer, - - playFilterHelpers - ) -) // endregion lazy val soadRoot = project.in(file(".")).settings( name := "SystemOfADownload" ).aggregate( - `artifact-api`, - `artifact-impl`, - `artifact-query-api`, - `artifact-query-impl`, - `versions-api`, - `versions-impl`, - `versions-query-api`, - `versions-query-impl`, - `version-synchronizer`, - `sonatype`, - `auth-api`, - `auth-impl`, - `server-auth`, ) ThisBuild / lagomCassandraEnabled := false diff --git a/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts new file mode 100644 index 00000000..58082c01 --- /dev/null +++ b/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("java") + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" + id("io.micronaut.application") version "3.7.0" + id("io.micronaut.test-resources") version "3.7.0" +} + +group = "org.spongepowered" +version = "1.0" +repositories { + mavenCentral() +} + +dependencies { + + id("com.github.johnrengelman.shadow") version "7.1.2" + id("io.micronaut.application") version "3.7.0" + id("io.micronaut.test-resources") version "3.7.0" +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java index 8aa8ad01..a066952c 100644 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java @@ -1,21 +1,28 @@ package org.spongepowered.downloads.artifacts; import akka.actor.typed.ActorRef; +import akka.actor.typed.Behavior; import akka.actor.typed.javadsl.AbstractBehavior; import akka.actor.typed.javadsl.ActorContext; +import akka.actor.typed.javadsl.Behaviors; import akka.actor.typed.javadsl.Receive; import org.spongepowered.downloads.artifacts.transport.GetArtifactDetailsResponse; import org.spongepowered.downloads.artifacts.transport.GetArtifactsResponse; import org.spongepowered.downloads.artifacts.transport.GroupResponse; import org.spongepowered.downloads.artifacts.transport.GroupsResponse; -class ArtifactQueries extends AbstractBehavior { +public class ArtifactQueries extends AbstractBehavior { + + public static Behavior create() { + return Behaviors.receive(Command.class) + .build(); + } public ArtifactQueries(final ActorContext context) { super(context); } - sealed interface Command { + public sealed interface Command { record GetGroup( String groupId, diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java index 513f7213..1733a99c 100644 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java +++ b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java @@ -53,6 +53,11 @@ private CompletionStage getArtifacts(String name) { artifactQueries, ref -> new ArtifactQueries.Command.GetArtifacts(name, ref), askTimeout, scheduler); } + private Route getGroup() { + return get(() -> onSuccess(getGroups(), groups -> + complete(StatusCodes.OK, groups, Jackson.marshaller()) + )); + } /** * This method creates one route (of possibly many more that will be part of your Web App) @@ -61,9 +66,7 @@ public Route artifactRoutes() { // v1/groups return pathPrefix("groups", () -> concat( - get(() -> onSuccess(getGroups(), groups -> - complete(StatusCodes.OK, groups, Jackson.marshaller()) - )), + getGroup(), // v1/groups/:groupId/ path(PathMatchers.segment(), (String groupId) -> concat( diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..52b199d6 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +micronautVersion=3.8.1 +akkaVersion =2.7.0 +scalaVersion=2.13 +akkaManagementVersion=1.2.0 +akkaProjection =1.3.1 +jacksonVersion = 2.14.2 +vavr = 0.10.4 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + 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/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f127cfd4 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@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/micronaut-cli.yml b/micronaut-cli.yml new file mode 100644 index 00000000..a41b0d8b --- /dev/null +++ b/micronaut-cli.yml @@ -0,0 +1,6 @@ +applicationType: default +defaultPackage: systemofadownload +testFramework: junit +sourceLanguage: java +buildTool: gradle_kotlin +features: [annotation-api, app-name, data, data-r2dbc, github-workflow-ci, github-workflow-docker-registry, github-workflow-graal-docker-registry, graalvm, gradle, hibernate-reactive-jpa, http-client, jackson-databind, jackson-xml, java, java-application, junit, liquibase, logback, micronaut-build, netty-server, openapi, postgres, r2dbc, reactor, reactor-http-client, readme, security-annotations, security-ldap, serialization-jackson, shade, swagger-ui, test-resources, toml, toml-build] diff --git a/openapi.properties b/openapi.properties new file mode 100644 index 00000000..c4a67c9b --- /dev/null +++ b/openapi.properties @@ -0,0 +1,6 @@ +swagger-ui.enabled=true +redoc.enabled=false +rapidoc.enabled=false +rapidoc.bg-color=#14191f +rapidoc.text-color=#aec2e0 +rapidoc.sort-endpoints-by=method diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..bb2f00f9 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,32 @@ + + +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } + plugins { + id("com.github.johnrengelman.shadow") version "7.1.2" + id("io.micronaut.application") version "3.7.0" + id("io.micronaut.test-resources") version "3.7.0" + } +} +rootProject.name="systemofadownload" + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) // needed for forge-loom, unfortunately + repositories { + mavenCentral() + maven("https://repo.spongepowered.org/repository/maven-public/") { + name = "sponge" + } + } +} + +include( + "artifacts", + "artifacts:api", + "artifacts:worker", + "artifacts:server", + "artifacts:events") +include("akka") diff --git a/src/main/java/systemofadownload/AkkaExtension.java b/src/main/java/systemofadownload/AkkaExtension.java new file mode 100644 index 00000000..25cd72da --- /dev/null +++ b/src/main/java/systemofadownload/AkkaExtension.java @@ -0,0 +1,56 @@ +package systemofadownload; + +import akka.actor.typed.ActorRef; +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Scheduler; +import akka.actor.typed.SpawnProtocol; +import akka.actor.typed.javadsl.Adapter; +import akka.actor.typed.javadsl.Behaviors; +import akka.cluster.sharding.typed.ClusterShardingSettings; +import akka.cluster.sharding.typed.ShardingEnvelope; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.cluster.sharding.typed.javadsl.Entity; +import akka.management.cluster.bootstrap.ClusterBootstrap; +import akka.management.javadsl.AkkaManagement; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import jakarta.inject.Singleton; + +@Factory +public class AkkaExtension { + + + @Bean + public Scheduler systemScheduler() { + return system().scheduler(); + } + + @Bean + public Config akkaConfig() { + return ConfigFactory.load(); + } + + @Bean(preDestroy = "terminate") + public ActorSystem system() { + Config config = akkaConfig(); + return ActorSystem.create( + Behaviors.setup(ctx -> { + akka.actor.ActorSystem unTypedSystem = Adapter.toClassic(ctx.getSystem()); + AkkaManagement.get(unTypedSystem).start(); + ClusterBootstrap.get(unTypedSystem).start(); + return SpawnProtocol.create(); + }), config.getString("some.cluster.name")); + } + + @Bean + public ClusterSharding clusterSharding() { + return ClusterSharding.get(system()); + } + + @Bean + public ActorRef> someShardRegion() { + return clusterSharding().init(Entity.of(null, null)); + } +} diff --git a/src/main/java/systemofadownload/Application.java b/src/main/java/systemofadownload/Application.java new file mode 100644 index 00000000..6b627497 --- /dev/null +++ b/src/main/java/systemofadownload/Application.java @@ -0,0 +1,18 @@ +package systemofadownload; + +import io.micronaut.runtime.Micronaut; +import io.swagger.v3.oas.annotations.*; +import io.swagger.v3.oas.annotations.info.*; + +@OpenAPIDefinition( + info = @Info( + title = "systemofadownload", + version = "0.0" + ) +) +public class Application { + + public static void main(String[] args) { + Micronaut.run(Application.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/systemofadownload/SystemofadownloadController.java b/src/main/java/systemofadownload/SystemofadownloadController.java new file mode 100644 index 00000000..a27608d5 --- /dev/null +++ b/src/main/java/systemofadownload/SystemofadownloadController.java @@ -0,0 +1,58 @@ +package systemofadownload; + +import akka.Done; +import akka.actor.typed.ActorRef; +import akka.actor.typed.Scheduler; +import akka.actor.typed.javadsl.AskPattern; +import akka.cluster.sharding.typed.ShardingEnvelope; +import io.micronaut.http.annotation.*; +import io.micronaut.security.annotation.Secured; +import io.micronaut.security.rules.SecurityRule; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.inject.Inject; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletionStage; + +@Controller("/systemofadownload") +public class SystemofadownloadController { + record Command() {} + + private final ActorRef> region; + private final Scheduler scheduler; + + @Inject + public SystemofadownloadController( + ActorRef> region, + Scheduler scheduler + ) { + this.region = region; + this.scheduler = scheduler; + + } + + @Get(uri="/", produces="text/plain") + @Secured(SecurityRule.IS_ANONYMOUS) + public String index() { + return "Hello world"; + } + + public static final Duration TIMEOUT = Duration.ofSeconds(10); + + public void someFireForgetMethod(){ + this.region.tell(new ShardingEnvelope<>("foo", new Command())); + } + + record Foo() {} + public Mono someNeedResponseMethod(){ + CompletionStage willBeResponse = AskPattern.ask( + this.region, + replyTo -> new ShardingEnvelope<>("entityId", new Command()), + TIMEOUT, + scheduler + ); + return Mono.fromCompletionStage(willBeResponse); + } +} diff --git a/src/main/java/systemofadownload/UnauthorizedHandler.java b/src/main/java/systemofadownload/UnauthorizedHandler.java new file mode 100644 index 00000000..f7c73c20 --- /dev/null +++ b/src/main/java/systemofadownload/UnauthorizedHandler.java @@ -0,0 +1,30 @@ +package systemofadownload; + +import io.micronaut.context.annotation.Replaces; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.security.authentication.AuthorizationException; +import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler; +import io.micronaut.serde.annotation.Serdeable; +import jakarta.inject.Singleton; + +import java.util.List; + +@Singleton +@Replaces(DefaultAuthorizationExceptionHandler.class) +public class UnauthorizedHandler extends DefaultAuthorizationExceptionHandler { + + @Override + public MutableHttpResponse handle(final HttpRequest request, final AuthorizationException exception) { + return super.handle(request, exception) + .body(new UnauthorizedError(401, List.of("Unauthorized"))); + } + + + @Serdeable + record UnauthorizedError( + int code, List errors + ) { + + } +} diff --git a/src/main/java/systemofadownload/artifacts/ArtifactController.java b/src/main/java/systemofadownload/artifacts/ArtifactController.java new file mode 100644 index 00000000..001967ae --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/ArtifactController.java @@ -0,0 +1,33 @@ +package systemofadownload.artifacts; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.QueryValue; +import systemofadownload.artifacts.api.query.ArtifactRegistration; +import systemofadownload.artifacts.api.query.GetArtifactsResponse; + +import java.util.concurrent.Flow; + +@Controller("/groups/{groupID}/artifacts") +public class ArtifactController { + + + @Post("/") + public HttpResponse createArtifact( + @PathVariable String groupID, + @Body ArtifactRegistration.RegisterArtifact registration + ) { + return null; + } + + @Get("/{artifactID}") + public Flow.Publisher> getArtifact( + @PathVariable String groupID, + @PathVariable String artifactID) { + return null; + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/Artifact.java b/src/main/java/systemofadownload/artifacts/api/Artifact.java new file mode 100644 index 00000000..58746d7d --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/Artifact.java @@ -0,0 +1,46 @@ +/* + * 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 systemofadownload.artifacts.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.net.URI; +import java.util.Optional; + +@JsonSerialize +public final record Artifact( + @JsonProperty Optional classifier, + @JsonProperty URI downloadUrl, + @JsonProperty String md5, + @JsonProperty String sha1, + @JsonProperty String extension +) { + @JsonCreator + public Artifact { + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java b/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java new file mode 100644 index 00000000..718b7f57 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java @@ -0,0 +1,43 @@ +/* + * 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 systemofadownload.artifacts.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.List; + +@JsonDeserialize +public final record ArtifactCollection( + @JsonProperty("assets") List components, + @JsonProperty("coordinates") MavenCoordinates coordinates +) { + + @JsonCreator + public ArtifactCollection { + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java b/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java new file mode 100644 index 00000000..1cd351fa --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java @@ -0,0 +1,66 @@ +/* + * 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 systemofadownload.artifacts.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import java.util.StringJoiner; + +@JsonDeserialize +public record ArtifactCoordinates( + @JsonProperty(required = true) String groupId, + @JsonProperty(required = true) String artifactId +) { + @JsonCreator + public ArtifactCoordinates { + } + + public MavenCoordinates version(String version) { + return MavenCoordinates.parse( + new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); + } + + public String asMavenString() { + return this.groupId() + ":" + this.artifactId(); + } + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String groupId() { + return groupId; + } + + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + public String artifactId() { + return artifactId; + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactService.java b/src/main/java/systemofadownload/artifacts/api/ArtifactService.java new file mode 100644 index 00000000..3f7773c4 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/ArtifactService.java @@ -0,0 +1,69 @@ +/* + * 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 systemofadownload.artifacts.api; + +import akka.NotUsed; +import systemofadownload.artifacts.api.event.ArtifactUpdate; +import systemofadownload.artifacts.api.event.GroupUpdate; +import systemofadownload.artifacts.api.query.ArtifactDetails; +import systemofadownload.artifacts.api.query.ArtifactRegistration; +import systemofadownload.artifacts.api.query.GetArtifactsResponse; +import systemofadownload.artifacts.api.query.GroupRegistration; +import systemofadownload.artifacts.api.query.GroupResponse; +import systemofadownload.artifacts.api.query.GroupsResponse; + +public interface ArtifactService { + + ServiceCall, ArtifactDetails.Response> updateDetails(String groupId, String artifactId); + + ServiceCall getGroup(String groupId); + + ServiceCall getGroups(); + + Topic groupTopic(); + + Topic artifactUpdate(); + + @Override + default Descriptor descriptor() { + return Service.named("artifacts") + .withCalls( + Service.restCall(Method.GET, "/artifacts/groups/:groupId", this::getGroup), + Service.restCall(Method.GET, "/artifacts/groups", this::getGroups), + Service.restCall(Method.POST, "/artifacts/groups", this::registerGroup), + Service.restCall(Method.GET, "/artifacts/groups/:groupId/artifacts", this::getArtifacts), + Service.restCall(Method.POST, "/artifacts/groups/:groupId/artifacts", this::registerArtifacts), + Service.restCall(Method.PATCH, "/artifacts/groups/:groupId/artifacts/:artifactId/update", this::updateDetails) + ) + .withTopics( + Service.topic("group-activity", this::groupTopic) + .withProperty(KafkaProperties.partitionKeyStrategy(), GroupUpdate::groupId), + Service.topic("artifact-details-update", this::artifactUpdate) + .withProperty(KafkaProperties.partitionKeyStrategy(), ArtifactUpdate::partitionKey) + ) + .withAutoAcl(true); + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/Group.java b/src/main/java/systemofadownload/artifacts/api/Group.java new file mode 100644 index 00000000..d42e7ee5 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/Group.java @@ -0,0 +1,42 @@ +/* + * 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 systemofadownload.artifacts.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize +public record Group( + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String website +) { + + @JsonCreator + public Group { + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java b/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java new file mode 100644 index 00000000..18ee831b --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java @@ -0,0 +1,192 @@ +/* + * 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 systemofadownload.artifacts.api; + +import com.fasterxml.jackson.annotation.JsonCreator; +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; +import java.util.regex.Pattern; + +@JsonDeserialize +public final class MavenCoordinates implements Comparable { + + private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); + + /** + * The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String groupId; + /** + * The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String artifactId; + /** + * The version of an artifact, as defined by the Apache Maven documentation. This is + * traditionally specified as a Maven repository searchable version string, such as + * {@code 1.0.0-SNAPSHOT}. + * See Maven Coordinates. + */ + @JsonProperty(required = true) + public final String version; + + @JsonIgnore + public final VersionType versionType; + + @JsonIgnore + private final ComparableVersion mavenVersion; + + /** + * Parses a set of maven formatted coordinates as per + * Apache + * Maven's documentation. + * + * @param coordinates The coordinates delimited by `:` + * @return A parsed set of MavenCoordinates + */ + public static MavenCoordinates parse(final String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + return new MavenCoordinates(groupId, artifactId, version); + } + + public MavenCoordinates(String coordinates) { + final var splitCoordinates = coordinates.split(":"); + if (splitCoordinates.length < 3) { + throw new IllegalArgumentException( + "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); + } + final var groupId = splitCoordinates[0]; + if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { + throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); + } + final var artifactId = splitCoordinates[1]; + if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { + throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); + } + final var version = splitCoordinates[2]; + + VersionType.fromVersion(version); // validates the version is going to be valid somewhat + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonCreator + public MavenCoordinates( + @JsonProperty("groupId") final String groupId, + @JsonProperty("artifactId") final String artifactId, + @JsonProperty("version") final String version + ) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.versionType = VersionType.fromVersion(version); + this.mavenVersion = new ComparableVersion(version); + } + + @JsonIgnore + public String asStandardCoordinates() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.versionType.asStandardVersionString(this.version)) + .toString(); + } + + @JsonIgnore + public boolean isSnapshot() { + return this.versionType.isSnapshot(); + } + + @JsonIgnore + public ArtifactCoordinates asArtifactCoordinates() { + return new ArtifactCoordinates(this.groupId, this.artifactId); + } + + @Override + public String toString() { + return new StringJoiner(":") + .add(this.groupId) + .add(this.artifactId) + .add(this.version) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final MavenCoordinates that = (MavenCoordinates) o; + return Objects.equals(this.groupId, that.groupId) && Objects.equals( + this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(this.groupId, this.artifactId, this.version); + } + + @Override + public int compareTo(final MavenCoordinates o) { + final var group = this.groupId.compareTo(o.groupId); + if (group != 0) { + return group; + } + final var artifact = this.artifactId.compareTo(o.artifactId); + if (artifact != 0) { + return artifact; + } + return this.mavenVersion.compareTo(o.mavenVersion); + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/VersionType.java b/src/main/java/systemofadownload/artifacts/api/VersionType.java new file mode 100644 index 00000000..fc9c8a5b --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/VersionType.java @@ -0,0 +1,115 @@ +/* + * 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 systemofadownload.artifacts.api; + +import java.util.StringJoiner; +import java.util.regex.Pattern; + +/** + * In conjunction with {@link MavenCoordinates}, can be used to determine the + * version type of the coordinates, and whether + */ +public enum VersionType { + /** + * A timestamp based file snapshot, such as {@code 1.0.0-20210118.163210-1} + * to where it can be interpreted that the {@link #SNAPSHOT snapshot} version + * would be {@code 1.0.0-SNAPSHOT} that happened to build at date time + * {@code January 18th, 2021 at 16h32m10s} and it's the first build. + */ + TIMESTAMP_SNAPSHOT { + @Override + public boolean isSnapshot() { + return true; + } + + @Override + public String asStandardVersionString(final String version) { + final var split = version.split("-"); + final var stringJoiner = new StringJoiner("-"); + for (int i = 0; i < split.length - 2; i++) { + stringJoiner.add(split[i]); + } + + return stringJoiner.add(SNAPSHOT_VERSION).toString(); + } + }, + + /** + * A standard generic snapshot relative version of a release, such as {@code 1.0.0-SNAPSHOT}. + */ + SNAPSHOT { + @Override + public boolean isSnapshot() { + return true; + } + }, + + /** + * A standard release version not abiding by any snapshot guidelines, considered + * final and singular, such as {@code 1.0.0} + */ + RELEASE; + + /* + Simple SNAPSHOT placeholder + */ + private static final String SNAPSHOT_VERSION = "SNAPSHOT"; + + /* + Verifies the pattern that the snapshot version is date.time-build formatted, + enables the pattern match for a timestamped snapshot + */ + private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-(\\d{8}.\\d{6})-(\\d+)$"); + + private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("(\\d{8}.\\d{6})-(\\d+)$"); + + public static VersionType fromVersion(final String version) { + if (version == null || version.isEmpty()) { + throw new IllegalArgumentException("Version cannot be empty"); + } + // Simple check to find out if the version ends with SNAPSHOT. + if (version.regionMatches( + true, + version.length() - SNAPSHOT_VERSION.length(), + SNAPSHOT_VERSION, + 0, + SNAPSHOT_VERSION.length() + )) { + return SNAPSHOT; + } + if (VERSION_FILE_PATTERN.matcher(version).matches()) { + return TIMESTAMP_SNAPSHOT; + } + return RELEASE; + } + + public boolean isSnapshot() { + return false; + } + + public String asStandardVersionString(final String version) { + return version; + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java b/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java new file mode 100644 index 00000000..7a88ccb6 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.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 systemofadownload.artifacts.api.event; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.serialization.Jsonable; +import systemofadownload.artifacts.api.ArtifactCoordinates; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(ArtifactUpdate.ArtifactRegistered.class), + @JsonSubTypes.Type(ArtifactUpdate.GitRepositoryAssociated.class), + @JsonSubTypes.Type(ArtifactUpdate.WebsiteUpdated.class), + @JsonSubTypes.Type(ArtifactUpdate.IssuesUpdated.class), + @JsonSubTypes.Type(ArtifactUpdate.DisplayNameUpdated.class), +}) +public interface ArtifactUpdate extends Jsonable { + + ArtifactCoordinates coordinates(); + + default String partitionKey() { + return this.coordinates().asMavenString(); + } + + @JsonTypeName("registered") + @JsonDeserialize + final record ArtifactRegistered( + ArtifactCoordinates coordinates + ) implements ArtifactUpdate { + + @JsonCreator + public ArtifactRegistered { + } + } + + @JsonTypeName("git-repository") + @JsonDeserialize + final record GitRepositoryAssociated( + ArtifactCoordinates coordinates, + String repository + ) implements ArtifactUpdate { + + @JsonCreator + public GitRepositoryAssociated { + } + } + + @JsonTypeName("website") + @JsonDeserialize + final record WebsiteUpdated( + ArtifactCoordinates coordinates, + String url + ) implements ArtifactUpdate { + + @JsonCreator + public WebsiteUpdated { + } + } + + @JsonTypeName("issues") + @JsonDeserialize + final record IssuesUpdated( + ArtifactCoordinates coordinates, + String url + ) implements ArtifactUpdate { + + @JsonCreator + public IssuesUpdated { + } + } + + @JsonTypeName("displayName") + @JsonDeserialize + final record DisplayNameUpdated( + ArtifactCoordinates coordinates, + String displayName + ) implements ArtifactUpdate { + + @JsonCreator + public DisplayNameUpdated { + } + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java b/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java new file mode 100644 index 00000000..06267ebd --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java @@ -0,0 +1,74 @@ +/* + * 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 systemofadownload.artifacts.api.event; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.lightbend.lagom.serialization.Jsonable; +import systemofadownload.artifacts.api.ArtifactCoordinates; + +import java.io.Serial; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(GroupUpdate.GroupRegistered.class), + @JsonSubTypes.Type(GroupUpdate.ArtifactRegistered.class), +}) +public interface GroupUpdate extends Jsonable { + + String groupId(); + + @JsonTypeName("group-registered") + @JsonDeserialize + record GroupRegistered(String groupId, String name, String website) + implements GroupUpdate { + + @JsonCreator + public GroupRegistered { + } + + } + + @JsonTypeName("artifact-registered") + @JsonDeserialize + final record ArtifactRegistered(ArtifactCoordinates coordinates) implements GroupUpdate { + + @Serial private static final long serialVersionUID = 6319289932327553919L; + + @JsonCreator + public ArtifactRegistered { + } + + + @Override + public String groupId() { + return this.coordinates.groupId(); + } + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java b/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java new file mode 100644 index 00000000..92aa234f --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java @@ -0,0 +1,129 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.lightbend.lagom.javadsl.api.transport.BadRequest; +import io.vavr.control.Either; +import io.vavr.control.Try; + +import java.net.URL; + +public final class ArtifactDetails { + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Update.Website.class, + name = "website"), + @JsonSubTypes.Type(value = Update.DisplayName.class, + name = "displayName"), + @JsonSubTypes.Type(value = Update.Issues.class, + name = "issues"), + @JsonSubTypes.Type(value = Update.GitRepository.class, + name = "gitRepository"), + }) + @JsonDeserialize + public sealed interface Update { + + Either validate(); + + record Website( + @JsonProperty(required = true) String website + ) implements Update { + + @JsonCreator + public Website { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.website())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.website()))); + } + } + + record DisplayName( + @JsonProperty(required = true) String display + ) implements Update { + + @JsonCreator + public DisplayName { + } + + @Override + public Either validate() { + return Either.right(this.display.trim()); + } + } + + record Issues( + @JsonProperty(required = true) String issues + ) implements Update { + @JsonCreator + public Issues { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.issues())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.issues()))); + } + } + + record GitRepository( + @JsonProperty(required = true) String gitRepo + ) implements Update { + + @JsonCreator + public GitRepository { + } + + @Override + public Either validate() { + return Try.of(() -> new URL(this.gitRepo())) + .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.gitRepo()))); + } + } + } + + @JsonSerialize + public record Response( + String name, + String displayName, + String website, + String issues, + String gitRepo + ) { + + } + + +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java b/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java new file mode 100644 index 00000000..b125349d --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java @@ -0,0 +1,81 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 systemofadownload.artifacts.api.ArtifactCoordinates; + +public final class ArtifactRegistration { + + @JsonSerialize + public record RegisterArtifact( + @JsonProperty(required = true) String artifactId, + @JsonProperty(required = true) String displayName + ) { + + @JsonCreator + public RegisterArtifact(final String artifactId, final String displayName) { + this.artifactId = artifactId; + this.displayName = displayName; + } + + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = Response.GroupMissing.class, + name = "UnknownGroup"), + @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, + name = "RegisteredArtifact"), + @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, + name = "AlreadyRegistered"), + }) + public sealed interface Response { + + @JsonSerialize + record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { + + } + + @JsonSerialize + record ArtifactAlreadyRegistered( + @JsonProperty String artifactName, + @JsonProperty String groupId + ) implements Response { + + } + + @JsonSerialize + record GroupMissing(@JsonProperty("groupId") String s) implements Response { + + } + + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java new file mode 100644 index 00000000..13c33681 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java @@ -0,0 +1,60 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 java.util.List; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GetArtifactsResponse.GroupMissing.class, name = "UnknownGroup"), + @JsonSubTypes.Type(value = GetArtifactsResponse.ArtifactsAvailable.class, name = "Artifacts"), +}) +public sealed interface GetArtifactsResponse { + + @JsonSerialize + record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { + + @JsonCreator + public GroupMissing { + } + + } + + @JsonSerialize + record ArtifactsAvailable(@JsonProperty List artifactIds) + implements GetArtifactsResponse { + + @JsonCreator + public ArtifactsAvailable { + } + + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java b/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java new file mode 100644 index 00000000..61e38c2d --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java @@ -0,0 +1,55 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import systemofadownload.artifacts.api.Group; + +public final class GroupRegistration { + + @JsonDeserialize + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website + ) { + + @JsonCreator + public RegisterGroupRequest { } + + } + + public interface Response { + + record GroupAlreadyRegistered(String groupNameRequested) implements Response { + } + + record GroupRegistered(Group group) implements Response { + + } + } +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java new file mode 100644 index 00000000..ca32acef --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java @@ -0,0 +1,61 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 com.lightbend.lagom.serialization.Jsonable; +import systemofadownload.artifacts.api.Group; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), + @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") +}) +public sealed interface GroupResponse extends Jsonable { + + @JsonSerialize + record Missing(@JsonProperty String groupId) implements GroupResponse { + @JsonCreator + public Missing(final String groupId) { + this.groupId = groupId; + } + + } + + @JsonSerialize + record Available(@JsonProperty Group group) implements GroupResponse { + + @JsonCreator + public Available(final Group group) { + this.group = group; + } + + } + +} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java new file mode 100644 index 00000000..5a0dd0ce --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java @@ -0,0 +1,48 @@ +/* + * 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 systemofadownload.artifacts.api.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +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 io.vavr.collection.List; +import systemofadownload.artifacts.api.Group; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") +}) +public interface GroupsResponse { + + @JsonSerialize + record Available(@JsonProperty List groups) + implements GroupsResponse { + @JsonCreator + public Available { + } + } +} diff --git a/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java b/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java new file mode 100644 index 00000000..0e4b9992 --- /dev/null +++ b/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java @@ -0,0 +1,23 @@ +package systemofadownload.artifacts.query; + +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import systemofadownload.artifacts.api.query.ArtifactRegistration; +import systemofadownload.artifacts.api.query.GetArtifactsResponse; + +import java.util.concurrent.Flow; + +@Controller("/groups/{groupID}/artifacts") +public class ArtifactQueryController { + + + @Get("/") + public Flow.Publisher> getArtifacts( + @PathVariable String groupID + ) { + return null; + } +} diff --git a/src/main/java/systemofadownload/groups/GroupController.java b/src/main/java/systemofadownload/groups/GroupController.java new file mode 100644 index 00000000..ddcbadd2 --- /dev/null +++ b/src/main/java/systemofadownload/groups/GroupController.java @@ -0,0 +1,41 @@ +package systemofadownload.groups; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.SpawnProtocol; +import akka.cluster.sharding.typed.javadsl.ClusterSharding; +import akka.stream.javadsl.AsPublisher; +import akka.stream.javadsl.JavaFlowSupport; +import akka.stream.javadsl.Source; +import akka.stream.typed.javadsl.ActorFlow; +import akka.stream.typed.javadsl.ActorSink; +import io.micronaut.context.annotation.Bean; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Post; +import jakarta.inject.Inject; +import systemofadownload.artifacts.api.query.GetArtifactsResponse; +import systemofadownload.artifacts.api.query.GroupRegistration; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Flow; + +@Controller("/groups") +public class GroupController { + + @Inject + private ActorSystem system; + @Inject + private ClusterSharding sharding; + + @Post("/") + public HttpResponse registerGroup( + @Body GroupRegistration.RegisterGroupRequest req + ) { + return null; + } + +} diff --git a/src/main/java/systemofadownload/groups/query/GroupsQueryController.java b/src/main/java/systemofadownload/groups/query/GroupsQueryController.java new file mode 100644 index 00000000..3e40b5ab --- /dev/null +++ b/src/main/java/systemofadownload/groups/query/GroupsQueryController.java @@ -0,0 +1,32 @@ +package systemofadownload.groups.query; + +import akka.actor.typed.ActorSystem; +import akka.stream.javadsl.AsPublisher; +import akka.stream.javadsl.JavaFlowSupport; +import akka.stream.javadsl.Source; +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 systemofadownload.artifacts.api.query.GetArtifactsResponse; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Flow; + +@Controller("/groups") +public class GroupsQueryController { + + @Inject + private ActorSystem system; + + @Get(uri = "/{groupID}") + public Flow.Publisher> g(@PathVariable String groupID) { + return Source.from(Arrays.asList("", "b")) + .via(akka.stream.javadsl.Flow.>fromFunction( + s -> HttpResponse.ok(new GetArtifactsResponse.ArtifactsAvailable(List.of(s))) + )) + .runWith(JavaFlowSupport.Sink.asPublisher(AsPublisher.WITH_FANOUT), this.system); + } +} diff --git a/src/main/resources/application.toml b/src/main/resources/application.toml new file mode 100644 index 00000000..4c3e2b17 --- /dev/null +++ b/src/main/resources/application.toml @@ -0,0 +1,20 @@ +micronaut.application.name = 'systemofadownload' +liquibase.datasources.default.change-log = 'classpath:db/liquibase-changelog.xml' +jpa.default.reactive = false +netty.default.allocator.max-order = 3 + +[r2dbc.datasources.default] +url = 'r2dbc:postgresql://localhost:5432/postgres' +username = 'postgres' +password = '' +dialect = 'POSTGRES' + +[micronaut.router.static-resources.swagger] +paths = 'classpath:META-INF/swagger' +mapping = '/swagger/**' + +[micronaut.router.static-resources.swagger-ui] +paths = 'classpath:META-INF/swagger/views/swagger-ui' +mapping = '/swagger-ui/**' + +micronaut.security.enabled=false diff --git a/src/main/resources/db/changelog/01-schema.xml b/src/main/resources/db/changelog/01-schema.xml new file mode 100644 index 00000000..7e9ab55b --- /dev/null +++ b/src/main/resources/db/changelog/01-schema.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/db/liquibase-changelog.xml b/src/main/resources/db/liquibase-changelog.xml new file mode 100644 index 00000000..468b33ab --- /dev/null +++ b/src/main/resources/db/liquibase-changelog.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 00000000..6010eb52 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + true + + + %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n + + + + + + + diff --git a/src/test/java/systemofadownload/SystemofadownloadTest.java b/src/test/java/systemofadownload/SystemofadownloadTest.java new file mode 100644 index 00000000..15fe3114 --- /dev/null +++ b/src/test/java/systemofadownload/SystemofadownloadTest.java @@ -0,0 +1,21 @@ +package systemofadownload; + +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import jakarta.inject.Inject; + +@MicronautTest(transactional = false) +class SystemofadownloadTest { + + @Inject + EmbeddedApplication application; + + @Test + void testItWorks() { + Assertions.assertTrue(application.isRunning()); + } + +} diff --git a/terraform/app/locals.tf b/terraform/app/locals.tf index 5366db39..3d994e4c 100644 --- a/terraform/app/locals.tf +++ b/terraform/app/locals.tf @@ -11,7 +11,7 @@ locals { } image = { replicas = 1 - image_version = "0.2-SNAPSHOT" + image_version = "latest" image_name = "ghcr.io/spongepowered/systemofadownload-artifact-impl" } extra_config = <<-EOF @@ -33,8 +33,8 @@ locals { service_name = "artifact-query" } image = { - replicas = var.environment == "dev" ? 1 : 1 - image_version = "0.2-SNAPSHOT" + replicas = 1 + image_version = "latest" image_name = "ghcr.io/spongepowered/systemofadownload-artifact-query-impl" } extra_config = <<-EOF @@ -61,9 +61,9 @@ locals { } "versions" = { service_name = "versions-server" - replicas = var.environment == "dev" ? 2 : 3 + replicas = 2 image = { - version = "0.2-SNAPSHOT" + version = "latest" name = "ghcr.io/spongepowered/systemofadownload-versions-impl" } extra_config = <<-EOF @@ -81,9 +81,9 @@ locals { } "versions_query" = { service_name = "versions-query-server" - replicas = var.environment == "dev" ? 1 : 1 + replicas = 1 image = { - version = "0.2-SNAPSHOT" + version = "latest" name = "ghcr.io/spongepowered/systemofadownload-versions-query-impl" } extra_config = <<-EOF @@ -93,9 +93,9 @@ locals { } "synchronizer" = { service_name = "version-synchronizer" - replicas = var.environment == "dev" ? 1 : 1 + replicas = 1 image = { - version = "0.2-SNAPSHOT" + version = "latest" name = "ghcr.io/spongepowered/systemofadownload-version-synchronizer" } extra_env = local.default_database_envs diff --git a/terraform/app/main.tf b/terraform/app/main.tf index 11dcb6c2..def3ae88 100644 --- a/terraform/app/main.tf +++ b/terraform/app/main.tf @@ -121,6 +121,7 @@ module "versions_syncrhonizer" { environment = var.environment app_image = local.soad_app_configs.synchronizer.image.name + memory_max = "2Gi" app_version = local.soad_app_configs.synchronizer.image.version app_name = local.soad_app_configs.synchronizer.service_name replica-count = local.soad_app_configs.synchronizer.replicas diff --git a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java new file mode 100644 index 00000000..6f687f1e --- /dev/null +++ b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java @@ -0,0 +1,6 @@ +package org.spongepowered.synchronizer.test.worker; + + +public class CommitDetailsRegistrarTest { + +} From 1de9317a404832e61d4281ce7360fbab4f53aeb1 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Sat, 9 Sep 2023 11:34:14 -0700 Subject: [PATCH 4/9] clean up micronaut framework integration Signed-off-by: Gabriel Harris-Rouquette --- .java-version | 2 +- .jvmopts | 1 + akka/build.gradle.kts | 32 +- akka/settings.gradle | 3 - akka/src/main/java/module-info.java | 5 - .../downloads/akka/AkkaExtension.java | 26 +- .../downloads/akka/AkkaSerializable.java | 7 + .../downloads/akka/ProductionAkkaSystem.java | 26 ++ akka/src/main/resources/refrerence.conf | 23 + akka/testkit/build.gradle.kts | 14 + .../test/akka/AkkaTestExtension.java | 44 ++ .../testkit/src/main/resources/reference.conf | 3 + .../downloads/artifact/api/Artifact.java | 46 -- .../artifact/api/ArtifactCollection.java | 42 -- .../artifact/api/ArtifactCoordinates.java | 66 --- .../artifact/api/ArtifactService.java | 83 ---- .../downloads/artifact/api/Group.java | 42 -- .../artifact/api/MavenCoordinates.java | 192 -------- .../downloads/artifact/api/VersionType.java | 115 ----- .../artifact/api/event/ArtifactUpdate.java | 110 ----- .../artifact/api/event/GroupUpdate.java | 74 --- .../artifact/api/query/ArtifactDetails.java | 129 ------ .../api/query/ArtifactRegistration.java | 82 ---- .../api/query/GetArtifactsResponse.java | 60 --- .../artifact/api/query/GroupRegistration.java | 56 --- .../artifact/api/query/GroupResponse.java | 61 --- .../artifact/api/query/GroupsResponse.java | 48 -- .../query/api/ArtifactQueryService.java | 45 -- .../query/api/GetArtifactDetailsResponse.java | 53 --- artifacts/README.md | 87 ---- artifacts/api/build.gradle.kts | 18 +- .../artifact/api/ArtifactCoordinates.java | 26 +- .../artifact/api/ArtifactService.java | 83 ---- .../artifact/api/query/ArtifactDetails.java | 22 +- .../api/query/ArtifactRegistration.java | 3 +- .../artifact/api/query/GroupRegistration.java | 17 +- .../artifact/api/query/GroupResponse.java | 10 +- artifacts/build.gradle.kts | 15 - artifacts/events/build.gradle.kts | 13 +- {akka => artifacts/events}/gradle.properties | 0 .../artifacts/events/ArtifactEvent.java | 5 +- .../artifacts/events}/DetailsEvent.java | 30 +- .../artifacts/events/GroupUpdate.java | 12 +- artifacts/gradle.properties | 1 - .../gradle/wrapper/gradle-wrapper.properties | 2 +- artifacts/micronaut-cli.yml | 6 - artifacts/server/build.gradle.kts | 108 ++--- .../artifacts/server/Application.java | 16 +- .../server/details/ArtifactDetailsEntity.java | 87 ++-- .../server/details/DetailsCommand.java | 3 +- .../server/details/DetailsManager.java | 136 ------ .../server/details/state/DetailsState.java | 6 +- .../server/details/state/PopulatedState.java | 5 +- .../server/global/GlobalCommand.java | 62 --- .../artifacts/server/global/GlobalEvent.java | 53 --- .../server/global/GlobalManager.java | 54 --- .../server/global/GlobalRegistration.java | 99 ---- .../artifacts/server/global/GlobalState.java | 38 -- .../artifacts/server/groups/GroupCommand.java | 63 --- .../artifacts/server/groups/GroupEntity.java | 206 --------- .../artifacts/server/groups/GroupEvent.java | 124 ----- .../artifacts/server/groups/GroupManager.java | 100 ---- .../server/groups/GroupsQueryController.java | 65 --- .../server/groups/state/EmptyState.java | 62 --- .../server/groups/state/GroupState.java | 47 -- .../server/groups/state/PopulatedState.java | 51 --- .../server/query/ArtifactsQuery.java | 12 - .../server/query/group/domain/GroupOrg.java | 27 ++ .../server/query/meta/ArtifactDto.java | 31 ++ .../query/meta/ArtifactQueryController.java | 52 +++ .../server/query/meta/ArtifactRepository.java | 22 + .../server/query/meta/domain/Group.java | 27 ++ .../query/meta/domain}/JpaArtifact.java | 108 ++--- .../meta/domain}/JpaArtifactTagValue.java | 51 +-- .../src/main/resources/application.conf | 32 ++ .../src/main/resources/application.toml | 10 - .../src/main/resources/application.yaml | 24 + .../changelog/01-create-artifacts-schema.xml | 125 +++++ .../main/resources/db/liquibase-changelog.xml | 10 + .../server/src/main/resources/logback.xml | 5 + .../server/ArtifactRepositoryTest.java | 118 +++++ .../src/test/resources/application-test.conf | 10 + .../src/test/resources/application-test.yaml | 22 + .../src/test}/resources/logback.xml | 7 +- .../artifacts/server/Application.java | 20 - .../server/query/ArtifactsQuery.java | 9 - .../server/query/GroupsQueryController.java | 28 -- artifacts/src/main/resources/application.toml | 10 - artifacts/src/main/resources/bootstrap.toml | 4 - .../downloads/artifacts/ArtifactsTest.java | 21 - artifacts/worker/build.gradle.kts | 79 ++-- .../worker/readside/ArtifactReadside.java | 102 +++++ .../worker/readside/JpaArtifact.java | 114 +++++ .../src/main/resources/application.conf | 32 ++ .../downloads/auth/api/AuthService.java | 54 --- .../auth/api/AuthenticationRequest.java | 36 -- .../downloads/auth/api/utils/AuthUtils.java | 144 ------ .../downloads/auth/AuthModule.java | 197 -------- .../downloads/auth/AuthServiceImpl.java | 81 ---- auth-impl/src/main/resources/application.conf | 20 - auth-impl/src/main/resources/logback.xml | 30 -- auth-impl/src/main/resources/reference.conf | 13 - build.gradle.kts | 132 ++---- build.sbt | 358 --------------- buildSrc/settings.gradle.kts | 9 + .../kotlin/soad.java-conventions.gradle.kts | 20 - dev/run_postgres.sh | 2 +- .../main/java/com/example/QuickstartApp.java | 62 --- .../main/java/com/example/UserRegistry.java | 101 ----- .../src/main/java/com/example/UserRoutes.java | 112 ----- downloads-api/src/main/java/module-info.java | 17 - .../spongepowered/downloads/api/Artifact.java | 46 -- .../downloads/api/ArtifactCollection.java | 42 -- .../downloads/api/ArtifactCoordinates.java | 66 --- .../spongepowered/downloads/api/Group.java | 42 -- .../downloads/api/MavenCoordinates.java | 192 -------- .../downloads/app/SystemOfADownloadsApp.java | 52 --- .../downloads/artifacts/ArtifactQueries.java | 59 --- .../downloads/artifacts/ArtifactRoutes.java | 92 ---- .../artifacts/transport/ArtifactDetails.java | 128 ------ .../transport/ArtifactRegistration.java | 82 ---- .../transport/GetArtifactDetailsResponse.java | 49 -- .../transport/GetArtifactsResponse.java | 40 -- .../transport/GroupRegistration.java | 56 --- .../artifacts/transport/GroupResponse.java | 36 -- .../artifacts/transport/GroupsResponse.java | 38 -- .../downloads/routes/VersionRoutes.java | 4 - .../downloads/versions/VersionQueries.java | 15 - .../downloads/versions/VersionRoutes.java | 72 --- .../versions/models/JpaTaggedVersion.java | 170 ------- .../models/JpaVersionedArtifactView.java | 231 ---------- .../versions/models/JpaVersionedAsset.java | 146 ------ .../models/JpaVersionedChangelog.java | 140 ------ .../versions/models/VersionedArtifactID.java | 59 --- .../versions/models/VersionedAssetID.java | 12 - .../versions/transport/QueryLatest.java | 46 -- .../versions/transport/QueryVersions.java | 57 --- .../versions/transport/TagCollection.java | 35 -- .../transport/VersionedChangelog.java | 65 --- .../versions/transport/VersionedCommit.java | 69 --- .../src/main/resources/application.conf | 6 - downloads-api/src/main/resources/logback.xml | 20 - .../test/java/com/example/UserRoutesTest.java | 77 ---- .../src/test/resources/application-test.conf | 3 - gradle.properties | 7 - gradle/libs.versions.toml | 46 ++ gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 19 +- gradlew.bat | 1 + liquibase/changelog/akka/akka_001_init.sql | 62 +++ liquibase/changelog/akka/akka_2_8_2.xml | 14 + liquibase/changelog/changelog.xml | 2 +- liquibase/changelog/lagom/lagom_1_6.xml | 54 --- project/build.properties | 1 - project/plugins.sbt | 8 - .../auth/AuthenticatedInternalService.java | 63 --- .../auth/InternalApplicationProfile.java | 35 -- .../downloads/auth/SOADAuth.java | 32 -- server-auth/src/main/resources/reference.conf | 16 - settings.gradle.kts | 20 +- .../downloads/maven/MavenConstants.java | 29 -- .../maven/artifact/ArtifactMavenMetadata.java | 86 ---- .../downloads/maven/artifact/Versioning.java | 90 ---- .../downloads/maven/snapshot/Snapshot.java | 36 -- .../maven/snapshot/SnapshotAsset.java | 86 ---- .../maven/snapshot/SnapshotMetadata.java | 42 -- .../maven/snapshot/SnapshotVersioning.java | 47 -- .../sonatype/AssetSearchResponse.java | 43 -- .../downloads/sonatype/Component.java | 111 ----- .../sonatype/ComponentSearchResponse.java | 140 ------ .../downloads/sonatype/MavenPom.java | 82 ---- .../downloads/maven/MavenMetadataTest.java | 95 ---- .../test/resources/maven-metadata-example.xml | 49 -- .../java/systemofadownload/AkkaExtension.java | 56 --- .../java/systemofadownload/Application.java | 18 - .../SystemofadownloadController.java | 58 --- .../UnauthorizedHandler.java | 30 -- .../artifacts/ArtifactController.java | 33 -- .../artifacts/api/Artifact.java | 46 -- .../artifacts/api/ArtifactCollection.java | 43 -- .../artifacts/api/ArtifactCoordinates.java | 66 --- .../artifacts/api/ArtifactService.java | 69 --- .../artifacts/api/Group.java | 42 -- .../artifacts/api/MavenCoordinates.java | 192 -------- .../artifacts/api/VersionType.java | 115 ----- .../artifacts/api/event/ArtifactUpdate.java | 110 ----- .../artifacts/api/event/GroupUpdate.java | 74 --- .../artifacts/api/query/ArtifactDetails.java | 129 ------ .../api/query/ArtifactRegistration.java | 81 ---- .../api/query/GetArtifactsResponse.java | 60 --- .../api/query/GroupRegistration.java | 55 --- .../artifacts/api/query/GroupResponse.java | 61 --- .../artifacts/api/query/GroupsResponse.java | 48 -- .../query/ArtifactQueryController.java | 23 - .../groups/GroupController.java | 41 -- .../groups/query/GroupsQueryController.java | 32 -- src/main/resources/application.toml | 20 - src/main/resources/db/changelog/01-schema.xml | 10 - src/main/resources/db/liquibase-changelog.xml | 8 - src/main/resources/logback.xml | 15 - .../SystemofadownloadTest.java | 21 - .../synchronizer/SonatypeSynchronizer.java | 95 ---- .../SynchronizationExtension.java | 47 -- .../synchronizer/SynchronizerModule.java | 95 ---- .../synchronizer/SynchronizerSettings.java | 97 ---- .../actor/ArtifactSyncExtension.java | 84 ---- .../actor/ArtifactSyncWorker.java | 134 ------ .../actor/CommitDetailsRegistrar.java | 84 ---- .../synchronizer/actor/CommitRegistrar.java | 94 ---- .../synchronizer/akka/FlowUtil.java | 122 ----- .../assetsync/AssetSettingsExtension.java | 69 --- .../assetsync/VersionConsumer.java | 74 --- .../assetsync/VersionedComponentWorker.java | 427 ------------------ .../gitmanaged/ArtifactSubscriber.java | 134 ------ .../gitmanaged/CommitConsumer.java | 140 ------ .../gitmanaged/ScheduledCommitResolver.java | 302 ------------- .../gitmanaged/domain/GitCommand.java | 144 ------ .../gitmanaged/domain/GitEvent.java | 83 ---- .../gitmanaged/domain/GitManagedArtifact.java | 141 ------ .../gitmanaged/domain/GitState.java | 222 --------- .../util/jgit/ActorLoggerPrinterWriter.java | 121 ----- .../util/jgit/AssetCommitResolver.java | 178 -------- .../util/jgit/CommitResolutionManager.java | 410 ----------------- .../util/jgit/FileWalkerConsumer.java | 68 --- .../util/jgit/RepositoryCloner.java | 270 ----------- .../util/jgit/StubSystemReader.java | 124 ----- .../resync/RequestArtifactsToSync.java | 90 ---- .../synchronizer/resync/ResyncExtension.java | 44 -- .../synchronizer/resync/ResyncManager.java | 148 ------ .../synchronizer/resync/ResyncSettings.java | 45 -- .../domain/ArtifactSynchronizerAggregate.java | 204 --------- .../synchronizer/resync/domain/Command.java | 82 ---- .../synchronizer/resync/domain/SyncState.java | 60 --- .../resync/domain/SynchronizeEvent.java | 53 --- .../versionsync/ArtifactConsumer.java | 81 ---- .../ArtifactVersionSyncEntity.java | 307 ------------- .../ArtifactVersionSyncModule.java | 57 --- .../versionsync/BatchVersionSyncManager.java | 89 ---- .../versionsync/SyncRegistration.java | 93 ---- .../versionsync/VersionRegistrationState.java | 336 -------------- .../versionsync/VersionSyncEvent.java | 77 ---- .../main/resources/META-INF/persistence.xml | 12 - .../src/main/resources/application.conf | 84 ---- .../src/main/resources/logback.xml | 40 -- .../src/main/resources/reference.conf | 25 - .../src/main/resources/soad.gitconfig | 0 .../worker/CommitDetailsRegistrarTest.java | 6 - .../worker/CommitResolutionManagerTest.java | 120 ----- .../test/worker/TestGitSystemReader.java | 77 ---- .../src/test/resources/dummy.gitconfig | 0 .../versions/api/VersionsService.java | 74 --- .../versions/api/models/ArtifactUpdate.java | 59 --- .../api/models/CommitRegistration.java | 61 --- .../versions/api/models/TagRegistration.java | 54 --- .../versions/api/models/TagVersion.java | 76 ---- .../api/models/VersionRegistration.java | 109 ----- .../api/models/VersionedArtifactUpdates.java | 62 --- .../api/models/VersionedChangelog.java | 65 --- .../versions/api/models/VersionedCommit.java | 70 --- .../api/models/tags/ArtifactTagEntry.java | 58 --- .../api/models/tags/ArtifactTagValue.java | 43 -- .../api/models/tags/VersionTagValue.java | 35 -- .../versions/server/VersionsModule.java | 75 --- .../versions/server/VersionsServiceImpl.java | 370 --------------- .../versions/server/domain/ACCommand.java | 82 ---- .../versions/server/domain/ACEvent.java | 134 ------ .../server/domain/InvalidRequest.java | 40 -- .../versions/server/domain/State.java | 223 --------- .../domain/VersionedArtifactAggregate.java | 189 -------- .../server/domain/VersionedArtifactEvent.java | 87 ---- .../readside/AssetReadsidePersistence.java | 120 ----- .../versions/server/readside/JpaArtifact.java | 186 -------- .../JpaArtifactRegexRecommendation.java | 85 ---- .../server/readside/JpaArtifactTag.java | 98 ---- .../server/readside/JpaArtifactVersion.java | 149 ------ .../readside/JpaVersionedArtifactAsset.java | 144 ------ .../readside/VersionReadSidePersistence.java | 201 --------- .../server/readside/VersionedTagWorker.java | 146 ------ .../versions/worker/EntityStore.java | 37 -- .../versions/worker/VersionConfig.java | 52 --- .../versions/worker/VersionExtension.java | 46 -- .../worker/VersionsWorkerSupervisor.java | 69 --- .../versions/worker/WorkerModule.java | 82 ---- .../versions/worker/WorkerSpawner.java | 75 --- .../actor/artifacts/CommitExtractor.java | 238 ---------- .../artifacts/FileCollectionOperator.java | 83 ---- .../artifacts/PotentiallyUsableAsset.java | 36 -- .../actor/delegates/RawCommitReceiver.java | 70 --- .../versionedartifact/ArtifactEvent.java | 82 ---- .../versionedartifact/ArtifactState.java | 249 ---------- .../VersionedArtifactCommand.java | 112 ----- .../VersionedArtifactEntity.java | 306 ------------- .../downloads/versions/worker/intro.md | 40 -- .../worker/readside/CommitProcessor.java | 214 --------- .../readside/model/JpaVersionChangelog.java | 132 ------ .../readside/model/JpaVersionedArtifact.java | 121 ----- .../main/resources/META-INF/persistence.xml | 21 - .../src/main/resources/application.conf | 42 -- versions-impl/src/main/resources/logback.xml | 38 -- .../src/main/resources/reference.conf | 8 - .../VersionedArtifactAggregateTest.java | 74 --- .../versions/worker/CommitExtractorTest.java | 61 --- .../worker/FileCollectionOperatorTest.java | 75 --- .../worker/VersionedArtifactEntityTest.java | 131 ------ .../downloads/versions/RegexValidations.java | 64 --- .../src/test/resources/application-test.conf | 11 - .../test/resources/bad-commit-test-jar.jar | Bin 359 -> 0 bytes versions-impl/src/test/resources/manifest | 3 - .../src/test/resources/no-commit-test-jar.jar | Bin 339 -> 0 bytes versions-impl/src/test/resources/test-jar.jar | Bin 388 -> 0 bytes .../query/api/VersionsQueryService.java | 70 --- .../query/api/models/QueryLatest.java | 46 -- .../query/api/models/QueryVersions.java | 57 --- .../query/api/models/TagCollection.java | 35 -- .../query/api/models/VersionedChangelog.java | 65 --- .../query/api/models/VersionedCommit.java | 69 --- .../query/impl/VersionQueryModule.java | 37 -- .../query/impl/VersionQueryServiceImpl.java | 324 ------------- .../query/impl/models/JpaTaggedVersion.java | 170 ------- .../impl/models/JpaVersionedArtifactView.java | 231 ---------- .../query/impl/models/JpaVersionedAsset.java | 144 ------ .../impl/models/JpaVersionedChangelog.java | 140 ------ .../impl/models/VersionedArtifactID.java | 59 --- .../main/resources/META-INF/persistence.xml | 16 - .../src/main/resources/application.conf | 29 -- .../src/main/resources/logback.xml | 37 -- 327 files changed, 1355 insertions(+), 22577 deletions(-) delete mode 100644 akka/settings.gradle delete mode 100644 akka/src/main/java/module-info.java create mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java create mode 100644 akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java create mode 100644 akka/src/main/resources/refrerence.conf create mode 100644 akka/testkit/build.gradle.kts create mode 100644 akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java create mode 100644 akka/testkit/src/main/resources/reference.conf delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/ArtifactUpdate.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java delete mode 100644 artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java delete mode 100644 artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/ArtifactQueryService.java delete mode 100644 artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java delete mode 100644 artifacts/README.md delete mode 100644 artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java rename {akka => artifacts/events}/gradle.properties (100%) rename artifacts/{server/src/main/java/org/spongepowered/downloads/artifacts/server/details => events/src/main/java/org/spongepowered/downloads/artifacts/events}/DetailsEvent.java (70%) delete mode 100644 artifacts/gradle.properties delete mode 100644 artifacts/micronaut-cli.yml delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java delete mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java create mode 100644 artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java rename {downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models => artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain}/JpaArtifact.java (54%) rename {downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models => artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain}/JpaArtifactTagValue.java (73%) create mode 100644 artifacts/server/src/main/resources/application.conf delete mode 100644 artifacts/server/src/main/resources/application.toml create mode 100644 artifacts/server/src/main/resources/application.yaml create mode 100644 artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml create mode 100644 artifacts/server/src/main/resources/db/liquibase-changelog.xml create mode 100644 artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java create mode 100644 artifacts/server/src/test/resources/application-test.conf create mode 100644 artifacts/server/src/test/resources/application-test.yaml rename artifacts/{src/main => server/src/test}/resources/logback.xml (84%) delete mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java delete mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java delete mode 100644 artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java delete mode 100644 artifacts/src/main/resources/application.toml delete mode 100644 artifacts/src/main/resources/bootstrap.toml delete mode 100644 artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java create mode 100644 artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java create mode 100644 artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/JpaArtifact.java create mode 100644 artifacts/worker/src/main/resources/application.conf delete mode 100644 auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthService.java delete mode 100644 auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthenticationRequest.java delete mode 100644 auth-api/src/main/java/org/spongepowered/downloads/auth/api/utils/AuthUtils.java delete mode 100644 auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthModule.java delete mode 100644 auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthServiceImpl.java delete mode 100644 auth-impl/src/main/resources/application.conf delete mode 100644 auth-impl/src/main/resources/logback.xml delete mode 100644 auth-impl/src/main/resources/reference.conf delete mode 100644 build.sbt create mode 100644 buildSrc/settings.gradle.kts delete mode 100644 buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts delete mode 100644 downloads-api/src/main/java/com/example/QuickstartApp.java delete mode 100644 downloads-api/src/main/java/com/example/UserRegistry.java delete mode 100644 downloads-api/src/main/java/com/example/UserRoutes.java delete mode 100644 downloads-api/src/main/java/module-info.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java delete mode 100644 downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java delete mode 100644 downloads-api/src/main/resources/application.conf delete mode 100644 downloads-api/src/main/resources/logback.xml delete mode 100644 downloads-api/src/test/java/com/example/UserRoutesTest.java delete mode 100644 downloads-api/src/test/resources/application-test.conf delete mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 liquibase/changelog/akka/akka_001_init.sql create mode 100644 liquibase/changelog/akka/akka_2_8_2.xml delete mode 100644 liquibase/changelog/lagom/lagom_1_6.xml delete mode 100644 project/build.properties delete mode 100644 project/plugins.sbt delete mode 100644 server-auth/src/main/java/org/spongepowered/downloads/auth/AuthenticatedInternalService.java delete mode 100644 server-auth/src/main/java/org/spongepowered/downloads/auth/InternalApplicationProfile.java delete mode 100644 server-auth/src/main/java/org/spongepowered/downloads/auth/SOADAuth.java delete mode 100644 server-auth/src/main/resources/reference.conf delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/MavenConstants.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/ArtifactMavenMetadata.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/Versioning.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/Snapshot.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotAsset.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotMetadata.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotVersioning.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/sonatype/AssetSearchResponse.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/sonatype/Component.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/sonatype/ComponentSearchResponse.java delete mode 100644 sonatype/src/main/java/org/spongepowered/downloads/sonatype/MavenPom.java delete mode 100644 sonatype/src/test/java/org/spongepowered/downloads/maven/MavenMetadataTest.java delete mode 100644 sonatype/src/test/resources/maven-metadata-example.xml delete mode 100644 src/main/java/systemofadownload/AkkaExtension.java delete mode 100644 src/main/java/systemofadownload/Application.java delete mode 100644 src/main/java/systemofadownload/SystemofadownloadController.java delete mode 100644 src/main/java/systemofadownload/UnauthorizedHandler.java delete mode 100644 src/main/java/systemofadownload/artifacts/ArtifactController.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/Artifact.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/ArtifactService.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/Group.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/VersionType.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java delete mode 100644 src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java delete mode 100644 src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java delete mode 100644 src/main/java/systemofadownload/groups/GroupController.java delete mode 100644 src/main/java/systemofadownload/groups/query/GroupsQueryController.java delete mode 100644 src/main/resources/application.toml delete mode 100644 src/main/resources/db/changelog/01-schema.xml delete mode 100644 src/main/resources/db/liquibase-changelog.xml delete mode 100644 src/main/resources/logback.xml delete mode 100644 src/test/java/systemofadownload/SystemofadownloadTest.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/SonatypeSynchronizer.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizationExtension.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerModule.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerSettings.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncExtension.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncWorker.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitDetailsRegistrar.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitRegistrar.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/akka/FlowUtil.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/AssetSettingsExtension.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionConsumer.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionedComponentWorker.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ArtifactSubscriber.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/CommitConsumer.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitCommand.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitManagedArtifact.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitState.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/ActorLoggerPrinterWriter.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/AssetCommitResolver.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/CommitResolutionManager.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/FileWalkerConsumer.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/StubSystemReader.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/RequestArtifactsToSync.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncExtension.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncSettings.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/Command.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SyncState.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SynchronizeEvent.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactConsumer.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncEntity.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncModule.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/BatchVersionSyncManager.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/SyncRegistration.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionRegistrationState.java delete mode 100644 version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionSyncEvent.java delete mode 100644 version-synchronizer/src/main/resources/META-INF/persistence.xml delete mode 100644 version-synchronizer/src/main/resources/application.conf delete mode 100644 version-synchronizer/src/main/resources/logback.xml delete mode 100644 version-synchronizer/src/main/resources/reference.conf delete mode 100644 version-synchronizer/src/main/resources/soad.gitconfig delete mode 100644 version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java delete mode 100644 version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java delete mode 100644 version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/TestGitSystemReader.java delete mode 100644 version-synchronizer/src/test/resources/dummy.gitconfig delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/VersionsService.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/ArtifactUpdate.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/CommitRegistration.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagRegistration.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagVersion.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionRegistration.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedArtifactUpdates.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedChangelog.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedCommit.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagEntry.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagValue.java delete mode 100644 versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/VersionTagValue.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsModule.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsServiceImpl.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACCommand.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACEvent.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/InvalidRequest.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/State.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactAggregate.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactEvent.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/AssetReadsidePersistence.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifact.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactRegexRecommendation.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactTag.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactVersion.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaVersionedArtifactAsset.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/EntityStore.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionConfig.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionExtension.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionsWorkerSupervisor.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerModule.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerSpawner.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/CommitExtractor.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/FileCollectionOperator.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/PotentiallyUsableAsset.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/delegates/RawCommitReceiver.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactEvent.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactState.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactCommand.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactEntity.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/intro.md delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/CommitProcessor.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionChangelog.java delete mode 100644 versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionedArtifact.java delete mode 100644 versions-impl/src/main/resources/META-INF/persistence.xml delete mode 100644 versions-impl/src/main/resources/application.conf delete mode 100644 versions-impl/src/main/resources/logback.xml delete mode 100644 versions-impl/src/main/resources/reference.conf delete mode 100644 versions-impl/src/test/java/org/spongepowered/downloads/test/server/collection/VersionedArtifactAggregateTest.java delete mode 100644 versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/CommitExtractorTest.java delete mode 100644 versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/FileCollectionOperatorTest.java delete mode 100644 versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/VersionedArtifactEntityTest.java delete mode 100644 versions-impl/src/test/java/org/spongepowered/downloads/versions/RegexValidations.java delete mode 100644 versions-impl/src/test/resources/application-test.conf delete mode 100644 versions-impl/src/test/resources/bad-commit-test-jar.jar delete mode 100644 versions-impl/src/test/resources/manifest delete mode 100644 versions-impl/src/test/resources/no-commit-test-jar.jar delete mode 100644 versions-impl/src/test/resources/test-jar.jar delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/VersionsQueryService.java delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryLatest.java delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryVersions.java delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/TagCollection.java delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedChangelog.java delete mode 100644 versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedCommit.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryModule.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaTaggedVersion.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedArtifactView.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedAsset.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedChangelog.java delete mode 100644 versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/VersionedArtifactID.java delete mode 100644 versions-query-impl/src/main/resources/META-INF/persistence.xml delete mode 100644 versions-query-impl/src/main/resources/application.conf delete mode 100644 versions-query-impl/src/main/resources/logback.xml diff --git a/.java-version b/.java-version index 46cbfbc7..fcc01369 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -graalvm64-17.0.4 +20.0.1 diff --git a/.jvmopts b/.jvmopts index b7359b71..a2c5f585 100644 --- a/.jvmopts +++ b/.jvmopts @@ -3,4 +3,5 @@ -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 +--enable-preview --add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/akka/build.gradle.kts b/akka/build.gradle.kts index 4ab025b9..99e22dcf 100644 --- a/akka/build.gradle.kts +++ b/akka/build.gradle.kts @@ -2,28 +2,16 @@ version = "0.1" group = "org.spongepowered.downloads" - -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project +plugins { + id("com.github.johnrengelman.shadow") + id("io.micronaut.library") +} dependencies { - implementation("com.ongres.scram:client:2.1") - implementation("jakarta.annotation:jakarta.annotation-api") - implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) - implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") - implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") - - runtimeOnly("ch.qos.logback:logback-classic") - compileOnly("org.graalvm.nativeimage:svm") - - implementation("io.micronaut:micronaut-validation") + 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/settings.gradle b/akka/settings.gradle deleted file mode 100644 index 766979b3..00000000 --- a/akka/settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ - -rootProject.name="akka" - diff --git a/akka/src/main/java/module-info.java b/akka/src/main/java/module-info.java deleted file mode 100644 index 4b98e64f..00000000 --- a/akka/src/main/java/module-info.java +++ /dev/null @@ -1,5 +0,0 @@ -module systemofadownload.akka { - requires akka.actor.typed; - requires akka.cluster.sharding; - exports org.spongepowered.downloads.akka; -} diff --git a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java index 1a232f7d..5f24ff44 100644 --- a/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java +++ b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaExtension.java @@ -1,6 +1,7 @@ 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.actor.typed.javadsl.Adapter; @@ -12,35 +13,32 @@ 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; @Factory public class AkkaExtension { @Bean - public Scheduler systemScheduler() { - return system().scheduler(); + public Scheduler systemScheduler(@NonNull ActorSystem system) { + return system.scheduler(); } @Bean public Config akkaConfig() { - return ConfigFactory.load(); + return ConfigFactory.defaultApplication(); } + @Singleton @Bean(preDestroy = "terminate") - public ActorSystem system() { - Config config = akkaConfig(); - return ActorSystem.create( - Behaviors.setup(ctx -> { - akka.actor.ActorSystem unTypedSystem = Adapter.toClassic(ctx.getSystem()); - AkkaManagement.get(unTypedSystem).start(); - ClusterBootstrap.get(unTypedSystem).start(); - return SpawnProtocol.create(); - }), config.getString("some.cluster.name")); + public ActorSystem system(@NonNull Behavior behavior, @NonNull Config config) { + return ActorSystem.create(behavior, "soad-master"); } @Bean - public ClusterSharding clusterSharding() { - return ClusterSharding.get(system()); + @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 new file mode 100644 index 00000000..793d772e --- /dev/null +++ b/akka/src/main/java/org/spongepowered/downloads/akka/AkkaSerializable.java @@ -0,0 +1,7 @@ +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 new file mode 100644 index 00000000..62c6baa1 --- /dev/null +++ b/akka/src/main/java/org/spongepowered/downloads/akka/ProductionAkkaSystem.java @@ -0,0 +1,26 @@ +package org.spongepowered.downloads.akka; + +import akka.actor.typed.ActorSystem; +import akka.actor.typed.Behavior; +import akka.actor.typed.SpawnProtocol; +import akka.actor.typed.javadsl.Behaviors; +import akka.management.cluster.bootstrap.ClusterBootstrap; +import akka.management.javadsl.AkkaManagement; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; + +@Factory +public class ProductionAkkaSystem { + + @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 new file mode 100644 index 00000000..7aa8e90a --- /dev/null +++ b/akka/src/main/resources/refrerence.conf @@ -0,0 +1,23 @@ + +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 new file mode 100644 index 00000000..ff4b171c --- /dev/null +++ b/akka/testkit/build.gradle.kts @@ -0,0 +1,14 @@ + + + +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) +} 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 new file mode 100644 index 00000000..69e37478 --- /dev/null +++ b/akka/testkit/src/main/java/org/spongepowered/downloads/test/akka/AkkaTestExtension.java @@ -0,0 +1,44 @@ +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 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.Replaces; +import io.micronaut.core.annotation.NonNull; +import jakarta.inject.Singleton; + +@Factory +public class AkkaTestExtension { + + @Replaces + @Bean + public Behavior testBehavior() { + return SpawnProtocol.create(); + } + + @Replaces + @Bean + public Config testConfig() { + return ConfigFactory.defaultApplication() + .withFallback(BehaviorTestKit.applicationTestConfig()) + .resolve(); + } + + @Bean(preDestroy = "shutdownTestKit") + public ActorTestKit testKit() { + return ActorTestKit.create(); + } + + @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 new file mode 100644 index 00000000..02455b35 --- /dev/null +++ b/akka/testkit/src/main/resources/reference.conf @@ -0,0 +1,3 @@ +systemofadownload { + clustering = false +} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java deleted file mode 100644 index 94c7ef77..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Artifact.java +++ /dev/null @@ -1,46 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import java.net.URI; -import java.util.Optional; - -@JsonSerialize -public final record Artifact( - @JsonProperty Optional classifier, - @JsonProperty URI downloadUrl, - @JsonProperty String md5, - @JsonProperty String sha1, - @JsonProperty String extension -) { - @JsonCreator - public Artifact { - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java deleted file mode 100644 index aab520db..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCollection.java +++ /dev/null @@ -1,42 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -@JsonDeserialize -public final record ArtifactCollection( - @JsonProperty("assets") List components, - @JsonProperty("coordinates") MavenCoordinates coordinates -) { - - @JsonCreator - public ArtifactCollection { - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java deleted file mode 100644 index 99131c11..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java +++ /dev/null @@ -1,66 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.util.StringJoiner; - -@JsonDeserialize -public record ArtifactCoordinates( - @JsonProperty(required = true) String groupId, - @JsonProperty(required = true) String artifactId -) { - @JsonCreator - public ArtifactCoordinates { - } - - public MavenCoordinates version(String version) { - return MavenCoordinates.parse( - new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); - } - - public String asMavenString() { - return this.groupId() + ":" + this.artifactId(); - } - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String groupId() { - return groupId; - } - - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String artifactId() { - return artifactId; - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java deleted file mode 100644 index 00508fc5..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java +++ /dev/null @@ -1,83 +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.artifact.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.broker.Topic; -import com.lightbend.lagom.javadsl.api.broker.kafka.KafkaProperties; -import com.lightbend.lagom.javadsl.api.transport.Method; -import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; -import org.spongepowered.downloads.artifact.api.event.GroupUpdate; -import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -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.query.GroupsResponse; - -public interface ArtifactService extends Service { - - ServiceCall getArtifacts(String groupId); - - ServiceCall registerArtifacts( - String groupId - ); - - ServiceCall registerGroup(); - - ServiceCall, ArtifactDetails.Response> updateDetails(String groupId, String artifactId); - - ServiceCall getGroup(String groupId); - - ServiceCall getGroups(); - - Topic groupTopic(); - - Topic artifactUpdate(); - - @Override - default Descriptor descriptor() { - return Service.named("artifacts") - .withCalls( - Service.restCall(Method.GET, "/artifacts/groups/:groupId", this::getGroup), - Service.restCall(Method.GET, "/artifacts/groups", this::getGroups), - Service.restCall(Method.POST, "/artifacts/groups", this::registerGroup), - Service.restCall(Method.GET, "/artifacts/groups/:groupId/artifacts", this::getArtifacts), - Service.restCall(Method.POST, "/artifacts/groups/:groupId/artifacts", this::registerArtifacts), - Service.restCall(Method.PATCH, "/artifacts/groups/:groupId/artifacts/:artifactId/update", this::updateDetails) - ) - .withTopics( - Service.topic("group-activity", this::groupTopic) - .withProperty(KafkaProperties.partitionKeyStrategy(), GroupUpdate::groupId), - Service.topic("artifact-details-update", this::artifactUpdate) - .withProperty(KafkaProperties.partitionKeyStrategy(), ArtifactUpdate::partitionKey) - ) - .withAutoAcl(true); - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java deleted file mode 100644 index 7e9145d1..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Group.java +++ /dev/null @@ -1,42 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record Group( - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String website -) { - - @JsonCreator - public Group { - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java deleted file mode 100644 index c1acb521..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/MavenCoordinates.java +++ /dev/null @@ -1,192 +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.artifact.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -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; -import java.util.regex.Pattern; - -@JsonDeserialize -public final class MavenCoordinates implements Comparable { - - private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String groupId; - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String artifactId; - /** - * The version of an artifact, as defined by the Apache Maven documentation. This is - * traditionally specified as a Maven repository searchable version string, such as - * {@code 1.0.0-SNAPSHOT}. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String version; - - @JsonIgnore - public final VersionType versionType; - - @JsonIgnore - private final ComparableVersion mavenVersion; - - /** - * Parses a set of maven formatted coordinates as per - * Apache - * Maven's documentation. - * - * @param coordinates The coordinates delimited by `:` - * @return A parsed set of MavenCoordinates - */ - public static MavenCoordinates parse(final String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - return new MavenCoordinates(groupId, artifactId, version); - } - - public MavenCoordinates(String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonCreator - public MavenCoordinates( - @JsonProperty("groupId") final String groupId, - @JsonProperty("artifactId") final String artifactId, - @JsonProperty("version") final String version - ) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonIgnore - public String asStandardCoordinates() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.versionType.asStandardVersionString(this.version)) - .toString(); - } - - @JsonIgnore - public boolean isSnapshot() { - return this.versionType.isSnapshot(); - } - - @JsonIgnore - public ArtifactCoordinates asArtifactCoordinates() { - return new ArtifactCoordinates(this.groupId, this.artifactId); - } - - @Override - public String toString() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.version) - .toString(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final MavenCoordinates that = (MavenCoordinates) o; - return Objects.equals(this.groupId, that.groupId) && Objects.equals( - this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.artifactId, this.version); - } - - @Override - public int compareTo(final MavenCoordinates o) { - final var group = this.groupId.compareTo(o.groupId); - if (group != 0) { - return group; - } - final var artifact = this.artifactId.compareTo(o.artifactId); - if (artifact != 0) { - return artifact; - } - return this.mavenVersion.compareTo(o.mavenVersion); - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java deleted file mode 100644 index 471c4a5c..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/VersionType.java +++ /dev/null @@ -1,115 +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.artifact.api; - -import java.util.StringJoiner; -import java.util.regex.Pattern; - -/** - * In conjunction with {@link MavenCoordinates}, can be used to determine the - * version type of the coordinates, and whether - */ -public enum VersionType { - /** - * A timestamp based file snapshot, such as {@code 1.0.0-20210118.163210-1} - * to where it can be interpreted that the {@link #SNAPSHOT snapshot} version - * would be {@code 1.0.0-SNAPSHOT} that happened to build at date time - * {@code January 18th, 2021 at 16h32m10s} and it's the first build. - */ - TIMESTAMP_SNAPSHOT { - @Override - public boolean isSnapshot() { - return true; - } - - @Override - public String asStandardVersionString(final String version) { - final var split = version.split("-"); - final var stringJoiner = new StringJoiner("-"); - for (int i = 0; i < split.length - 2; i++) { - stringJoiner.add(split[i]); - } - - return stringJoiner.add(SNAPSHOT_VERSION).toString(); - } - }, - - /** - * A standard generic snapshot relative version of a release, such as {@code 1.0.0-SNAPSHOT}. - */ - SNAPSHOT { - @Override - public boolean isSnapshot() { - return true; - } - }, - - /** - * A standard release version not abiding by any snapshot guidelines, considered - * final and singular, such as {@code 1.0.0} - */ - RELEASE; - - /* - Simple SNAPSHOT placeholder - */ - private static final String SNAPSHOT_VERSION = "SNAPSHOT"; - - /* - Verifies the pattern that the snapshot version is date.time-build formatted, - enables the pattern match for a timestamped snapshot - */ - private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-(\\d{8}.\\d{6})-(\\d+)$"); - - private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("(\\d{8}.\\d{6})-(\\d+)$"); - - public static VersionType fromVersion(final String version) { - if (version == null || version.isEmpty()) { - throw new IllegalArgumentException("Version cannot be empty"); - } - // Simple check to find out if the version ends with SNAPSHOT. - if (version.regionMatches( - true, - version.length() - SNAPSHOT_VERSION.length(), - SNAPSHOT_VERSION, - 0, - SNAPSHOT_VERSION.length() - )) { - return SNAPSHOT; - } - if (VERSION_FILE_PATTERN.matcher(version).matches()) { - return TIMESTAMP_SNAPSHOT; - } - return RELEASE; - } - - public boolean isSnapshot() { - return false; - } - - public String asStandardVersionString(final String version) { - return version; - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/ArtifactUpdate.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/ArtifactUpdate.java deleted file mode 100644 index 828ea500..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/ArtifactUpdate.java +++ /dev/null @@ -1,110 +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.artifact.api.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(ArtifactUpdate.ArtifactRegistered.class), - @JsonSubTypes.Type(ArtifactUpdate.GitRepositoryAssociated.class), - @JsonSubTypes.Type(ArtifactUpdate.WebsiteUpdated.class), - @JsonSubTypes.Type(ArtifactUpdate.IssuesUpdated.class), - @JsonSubTypes.Type(ArtifactUpdate.DisplayNameUpdated.class), -}) -public interface ArtifactUpdate extends Jsonable { - - ArtifactCoordinates coordinates(); - - default String partitionKey() { - return this.coordinates().asMavenString(); - } - - @JsonTypeName("registered") - @JsonDeserialize - final record ArtifactRegistered( - ArtifactCoordinates coordinates - ) implements ArtifactUpdate { - - @JsonCreator - public ArtifactRegistered { - } - } - - @JsonTypeName("git-repository") - @JsonDeserialize - final record GitRepositoryAssociated( - ArtifactCoordinates coordinates, - String repository - ) implements ArtifactUpdate { - - @JsonCreator - public GitRepositoryAssociated { - } - } - - @JsonTypeName("website") - @JsonDeserialize - final record WebsiteUpdated( - ArtifactCoordinates coordinates, - String url - ) implements ArtifactUpdate { - - @JsonCreator - public WebsiteUpdated { - } - } - - @JsonTypeName("issues") - @JsonDeserialize - final record IssuesUpdated( - ArtifactCoordinates coordinates, - String url - ) implements ArtifactUpdate { - - @JsonCreator - public IssuesUpdated { - } - } - - @JsonTypeName("displayName") - @JsonDeserialize - final record DisplayNameUpdated( - ArtifactCoordinates coordinates, - String displayName - ) implements ArtifactUpdate { - - @JsonCreator - public DisplayNameUpdated { - } - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java deleted file mode 100644 index 970a873e..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/event/GroupUpdate.java +++ /dev/null @@ -1,74 +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.artifact.api.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -import java.io.Serial; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(GroupUpdate.GroupRegistered.class), - @JsonSubTypes.Type(GroupUpdate.ArtifactRegistered.class), -}) -public interface GroupUpdate extends Jsonable { - - String groupId(); - - @JsonTypeName("group-registered") - @JsonDeserialize - record GroupRegistered(String groupId, String name, String website) - implements GroupUpdate { - - @JsonCreator - public GroupRegistered { - } - - } - - @JsonTypeName("artifact-registered") - @JsonDeserialize - final record ArtifactRegistered(ArtifactCoordinates coordinates) implements GroupUpdate { - - @Serial private static final long serialVersionUID = 6319289932327553919L; - - @JsonCreator - public ArtifactRegistered { - } - - - @Override - public String groupId() { - return this.coordinates.groupId(); - } - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java deleted file mode 100644 index 53c9567f..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactDetails.java +++ /dev/null @@ -1,129 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.javadsl.api.transport.BadRequest; -import io.vavr.control.Either; -import io.vavr.control.Try; - -import java.net.URL; - -public final class ArtifactDetails { - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Update.Website.class, - name = "website"), - @JsonSubTypes.Type(value = Update.DisplayName.class, - name = "displayName"), - @JsonSubTypes.Type(value = Update.Issues.class, - name = "issues"), - @JsonSubTypes.Type(value = Update.GitRepository.class, - name = "gitRepository"), - }) - @JsonDeserialize - public sealed interface Update { - - Either validate(); - - record Website( - @JsonProperty(required = true) String website - ) implements Update { - - @JsonCreator - public Website { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.website())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.website()))); - } - } - - record DisplayName( - @JsonProperty(required = true) String display - ) implements Update { - - @JsonCreator - public DisplayName { - } - - @Override - public Either validate() { - return Either.right(this.display.trim()); - } - } - - record Issues( - @JsonProperty(required = true) String issues - ) implements Update { - @JsonCreator - public Issues { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.issues())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.issues()))); - } - } - - record GitRepository( - @JsonProperty(required = true) String gitRepo - ) implements Update { - - @JsonCreator - public GitRepository { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.gitRepo())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.gitRepo()))); - } - } - } - - @JsonSerialize - public record Response( - String name, - String displayName, - String website, - String issues, - String gitRepo - ) { - - } - - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java deleted file mode 100644 index 362cdd88..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java +++ /dev/null @@ -1,82 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -public final class ArtifactRegistration { - - @JsonSerialize - public record RegisterArtifact( - @JsonProperty(required = true) String artifactId, - @JsonProperty(required = true) String displayName - ) { - - @JsonCreator - public RegisterArtifact(final String artifactId, final String displayName) { - this.artifactId = artifactId; - this.displayName = displayName; - } - - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, - name = "UnknownGroup"), - @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, - name = "RegisteredArtifact"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, - name = "AlreadyRegistered"), - }) - public sealed interface Response extends Jsonable { - - @JsonSerialize - record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { - - } - - @JsonSerialize - record ArtifactAlreadyRegistered( - @JsonProperty String artifactName, - @JsonProperty String groupId - ) implements Response { - - } - - @JsonSerialize - record GroupMissing(@JsonProperty("groupId") String s) implements Response { - - } - - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java deleted file mode 100644 index eec64a44..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GetArtifactsResponse.java +++ /dev/null @@ -1,60 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GetArtifactsResponse.GroupMissing.class, name = "UnknownGroup"), - @JsonSubTypes.Type(value = GetArtifactsResponse.ArtifactsAvailable.class, name = "Artifacts"), -}) -public sealed interface GetArtifactsResponse extends Jsonable { - - @JsonSerialize - record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { - - @JsonCreator - public GroupMissing { - } - - } - - @JsonSerialize - record ArtifactsAvailable(@JsonProperty List artifactIds) - implements GetArtifactsResponse { - - @JsonCreator - public ArtifactsAvailable { - } - - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java deleted file mode 100644 index b9d21ab4..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupRegistration.java +++ /dev/null @@ -1,56 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.Group; - -public final class GroupRegistration { - - @JsonDeserialize - public record RegisterGroupRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String website - ) { - - @JsonCreator - public RegisterGroupRequest { } - - } - - public interface Response extends Jsonable { - - record GroupAlreadyRegistered(String groupNameRequested) implements Response { - } - - record GroupRegistered(Group group) implements Response { - - } - } -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java deleted file mode 100644 index a973e23f..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java +++ /dev/null @@ -1,61 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.Group; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), - @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") -}) -public sealed interface GroupResponse extends Jsonable { - - @JsonSerialize - record Missing(@JsonProperty String groupId) implements GroupResponse { - @JsonCreator - public Missing(final String groupId) { - this.groupId = groupId; - } - - } - - @JsonSerialize - record Available(@JsonProperty Group group) implements GroupResponse { - - @JsonCreator - public Available(final Group group) { - this.group = group; - } - - } - -} diff --git a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java b/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java deleted file mode 100644 index 78416658..00000000 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupsResponse.java +++ /dev/null @@ -1,48 +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.artifact.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.Group; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") -}) -public interface GroupsResponse { - - @JsonSerialize - record Available(@JsonProperty List groups) - implements GroupsResponse { - @JsonCreator - public Available { - } - } -} diff --git a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/ArtifactQueryService.java b/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/ArtifactQueryService.java deleted file mode 100644 index 7e002ddc..00000000 --- a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/ArtifactQueryService.java +++ /dev/null @@ -1,45 +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.query.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.transport.Method; - -public interface ArtifactQueryService extends Service { - - ServiceCall getArtifactDetails(String groupId, String artifactId); - - @Override - default Descriptor descriptor() { - return Service.named("artifact-query") - .withCalls( - Service.restCall(Method.GET, "/artifacts-query/groups/:groupId/artifacts/:artifactId", this::getArtifactDetails) - ) - .withAutoAcl(true); - } -} diff --git a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java b/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.java deleted file mode 100644 index 5113f526..00000000 --- a/artifact-query-api/src/main/java/org/spongepowered/downloads/artifacts/query/api/GetArtifactDetailsResponse.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.query.api; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.Map; -import io.vavr.collection.SortedSet; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GetArtifactDetailsResponse.RetrievedArtifact.class, - name = "latest") -}) -public sealed interface GetArtifactDetailsResponse { - - @JsonSerialize - record RetrievedArtifact( - ArtifactCoordinates coordinates, - String displayName, - String website, - String gitRepository, - String issues, - Map> tags - ) implements GetArtifactDetailsResponse { - - } -} diff --git a/artifacts/README.md b/artifacts/README.md deleted file mode 100644 index 47a547a0..00000000 --- a/artifacts/README.md +++ /dev/null @@ -1,87 +0,0 @@ -## Micronaut 3.8.2 Documentation - -- [User Guide](https://docs.micronaut.io/3.8.2/guide/index.html) -- [API Reference](https://docs.micronaut.io/3.8.2/api/index.html) -- [Configuration Reference](https://docs.micronaut.io/3.8.2/guide/configurationreference.html) -- [Micronaut Guides](https://guides.micronaut.io/index.html) ---- - -- [Shadow Gradle Plugin](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) -## Feature github-workflow-ci documentation - -- [https://docs.github.com/en/actions](https://docs.github.com/en/actions) - - -## Feature test-resources documentation - -- [Micronaut Test Resources documentation](https://micronaut-projects.github.io/micronaut-test-resources/latest/guide/) - - -## Feature micronaut-aot documentation - -- [Micronaut AOT documentation](https://micronaut-projects.github.io/micronaut-aot/latest/guide/) - - -## Feature security-ldap documentation - -- [Micronaut Security LDAP documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html#ldap) - - -## Feature security-jwt documentation - -- [Micronaut Security JWT documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html) - - -## Feature data-r2dbc documentation - -- [Micronaut Data R2DBC documentation](https://micronaut-projects.github.io/micronaut-data/latest/guide/#dbc) - -- [https://r2dbc.io](https://r2dbc.io) - - -## Feature kafka documentation - -- [Micronaut Kafka Messaging documentation](https://micronaut-projects.github.io/micronaut-kafka/latest/guide/index.html) - - -## Feature openapi documentation - -- [Micronaut OpenAPI Support documentation](https://micronaut-projects.github.io/micronaut-openapi/latest/guide/index.html) - -- [https://www.openapis.org](https://www.openapis.org) - - -## Feature cache-caffeine documentation - -- [Micronaut Caffeine Cache documentation](https://micronaut-projects.github.io/micronaut-cache/latest/guide/index.html) - -- [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) - - -## Feature r2dbc documentation - -- [Micronaut R2DBC documentation](https://micronaut-projects.github.io/micronaut-r2dbc/latest/guide/) - -- [https://r2dbc.io](https://r2dbc.io) - - -## Feature junit-params documentation - -- [https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests) - - -## Feature discovery-kubernetes documentation - -- [Micronaut Kubernetes Service Discovery documentation](https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/#service-discovery) - - -## Feature http-client documentation - -- [Micronaut HTTP Client documentation](https://docs.micronaut.io/latest/guide/index.html#httpClient) - - -## Feature serialization-jackson documentation - -- [Micronaut Serialization Jackson Core documentation](https://micronaut-projects.github.io/micronaut-serialization/latest/guide/) - - diff --git a/artifacts/api/build.gradle.kts b/artifacts/api/build.gradle.kts index 399e5cbf..23911298 100644 --- a/artifacts/api/build.gradle.kts +++ b/artifacts/api/build.gradle.kts @@ -1,9 +1,15 @@ -val jacksonVersion:String by project -dependencies { - api(platform("com.fasterxml.jackson:jackson-bom:${jacksonVersion}")) - api("com.fasterxml.jackson:jackson-core") - api("com.fasterxml.jackson.core:jackson-databind") - api("com.fasterxml.jackson.core:jackson-annotations") +version = "0.1" +group = "org.spongepowered.downloads" + +plugins { + `java-library` +} + +dependencies { + api(platform(libs.jacksonBom)) + api(libs.bundles.serder) + api(libs.maven) + api(libs.vavr) } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java index 99131c11..4bfc99db 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactCoordinates.java @@ -30,6 +30,17 @@ import java.util.StringJoiner; +/** + * Representation of a simplified {@link MavenCoordinates} with representation + * of only the {@link #groupId()} and {@link #artifactId()}. In general, this is + * to represent an artifact as a whole, rather than any specific version or + * variant. + * + * @param groupId The group id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + * @param artifactId The artifact id of an artifact, as defined by the Apache Maven documentation. + * See Maven Coordinates. + */ @JsonDeserialize public record ArtifactCoordinates( @JsonProperty(required = true) String groupId, @@ -48,19 +59,4 @@ public String asMavenString() { return this.groupId() + ":" + this.artifactId(); } - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String groupId() { - return groupId; - } - - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String artifactId() { - return artifactId; - } } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java deleted file mode 100644 index 00508fc5..00000000 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/ArtifactService.java +++ /dev/null @@ -1,83 +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.artifact.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.broker.Topic; -import com.lightbend.lagom.javadsl.api.broker.kafka.KafkaProperties; -import com.lightbend.lagom.javadsl.api.transport.Method; -import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; -import org.spongepowered.downloads.artifact.api.event.GroupUpdate; -import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -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.query.GroupsResponse; - -public interface ArtifactService extends Service { - - ServiceCall getArtifacts(String groupId); - - ServiceCall registerArtifacts( - String groupId - ); - - ServiceCall registerGroup(); - - ServiceCall, ArtifactDetails.Response> updateDetails(String groupId, String artifactId); - - ServiceCall getGroup(String groupId); - - ServiceCall getGroups(); - - Topic groupTopic(); - - Topic artifactUpdate(); - - @Override - default Descriptor descriptor() { - return Service.named("artifacts") - .withCalls( - Service.restCall(Method.GET, "/artifacts/groups/:groupId", this::getGroup), - Service.restCall(Method.GET, "/artifacts/groups", this::getGroups), - Service.restCall(Method.POST, "/artifacts/groups", this::registerGroup), - Service.restCall(Method.GET, "/artifacts/groups/:groupId/artifacts", this::getArtifacts), - Service.restCall(Method.POST, "/artifacts/groups/:groupId/artifacts", this::registerArtifacts), - Service.restCall(Method.PATCH, "/artifacts/groups/:groupId/artifacts/:artifactId/update", this::updateDetails) - ) - .withTopics( - Service.topic("group-activity", this::groupTopic) - .withProperty(KafkaProperties.partitionKeyStrategy(), GroupUpdate::groupId), - Service.topic("artifact-details-update", this::artifactUpdate) - .withProperty(KafkaProperties.partitionKeyStrategy(), ArtifactUpdate::partitionKey) - ) - .withAutoAcl(true); - } - -} 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 27247571..56e6a717 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 @@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -86,15 +85,22 @@ record GitRepository( } @JsonSerialize - public record Response( - String name, - String displayName, - String website, - String issues, - String gitRepo - ) { + @JsonTypeInfo(use = JsonTypeInfo.Id.NONE) + public sealed interface Response { + record Ok( + String name, + String displayName, + String website, + String issues, + String gitRepo + ) implements Response{ + + } + + record NotFound(String message) implements Response {} } + } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java index 362cdd88..d7f0cc82 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/ArtifactRegistration.java @@ -29,7 +29,6 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; public final class ArtifactRegistration { @@ -58,7 +57,7 @@ public RegisterArtifact(final String artifactId, final String displayName) { @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, name = "AlreadyRegistered"), }) - public sealed interface Response extends Jsonable { + public sealed interface Response { @JsonSerialize record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) 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 040345fb..b127e321 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 @@ -32,18 +32,19 @@ public final class GroupRegistration { @JsonDeserialize - public record RegisterGroupRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String website + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website ) { - @JsonCreator - public RegisterGroupRequest { } - + @JsonCreator + public RegisterGroupRequest { } - public interface Response { + } + + public sealed interface Response { record GroupAlreadyRegistered(String groupNameRequested) implements Response { } diff --git a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java index a973e23f..628fc841 100644 --- a/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java +++ b/artifacts/api/src/main/java/org/spongepowered/downloads/artifact/api/query/GroupResponse.java @@ -28,16 +28,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.serialization.Jsonable; import org.spongepowered.downloads.artifact.api.Group; -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), - @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") -}) -public sealed interface GroupResponse extends Jsonable { +@JsonTypeInfo(use = JsonTypeInfo.Id.NONE) +public sealed interface GroupResponse { @JsonSerialize record Missing(@JsonProperty String groupId) implements GroupResponse { diff --git a/artifacts/build.gradle.kts b/artifacts/build.gradle.kts index 8c40ab6c..b28b04f6 100644 --- a/artifacts/build.gradle.kts +++ b/artifacts/build.gradle.kts @@ -1,18 +1,3 @@ -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project - -subprojects { - dependencies { - implementation(project(":akka")) - } -} -dependencies { - -} - - diff --git a/artifacts/events/build.gradle.kts b/artifacts/events/build.gradle.kts index 61ccd4eb..d2abb374 100644 --- a/artifacts/events/build.gradle.kts +++ b/artifacts/events/build.gradle.kts @@ -1,12 +1,17 @@ -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project +plugins { + `java-library` +} + +java { + sourceCompatibility = JavaVersion.toVersion("20") + targetCompatibility = JavaVersion.toVersion("20") +} dependencies { api(project(":artifacts:api")) + api(project(":akka")) } diff --git a/akka/gradle.properties b/artifacts/events/gradle.properties similarity index 100% rename from akka/gradle.properties rename to artifacts/events/gradle.properties 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 f2fe681c..7afa5009 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 @@ -1,11 +1,14 @@ package org.spongepowered.downloads.artifacts.events; import com.fasterxml.jackson.annotation.JsonCreator; +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; -public sealed interface ArtifactEvent { +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public sealed interface ArtifactEvent extends AkkaSerializable { ArtifactCoordinates coordinates(); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java similarity index 70% rename from artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java rename to artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java index 80886e24..eadddd99 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsEvent.java +++ b/artifacts/events/src/main/java/org/spongepowered/downloads/artifacts/events/DetailsEvent.java @@ -22,37 +22,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.server.details; +package org.spongepowered.downloads.artifacts.events; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = DetailsEvent.ArtifactRegistered.class, name = "registered"), - @JsonSubTypes.Type(value = DetailsEvent.ArtifactDetailsUpdated.class, name = "details"), - @JsonSubTypes.Type(value = DetailsEvent.ArtifactIssuesUpdated.class, name = "issues"), - @JsonSubTypes.Type(value = DetailsEvent.ArtifactGitRepositoryUpdated.class, name = "git-repo"), - @JsonSubTypes.Type(value = DetailsEvent.ArtifactWebsiteUpdated.class, name = "website"), -}) -public interface DetailsEvent extends AggregateEvent, Jsonable { - - AggregateEventShards TAG = AggregateEventTag.sharded(DetailsEvent.class, 3); - - @Override - default AggregateEventTagger aggregateTag() { - return TAG; - } +public interface DetailsEvent extends AkkaSerializable { @JsonDeserialize + @JsonTypeName("registered") record ArtifactRegistered( ArtifactCoordinates coordinates ) implements DetailsEvent { @@ -62,6 +46,7 @@ record ArtifactRegistered( } @JsonDeserialize + @JsonTypeName("details") record ArtifactDetailsUpdated( ArtifactCoordinates coordinates, String displayName @@ -73,6 +58,7 @@ record ArtifactDetailsUpdated( } @JsonDeserialize + @JsonTypeName("issues") record ArtifactIssuesUpdated( ArtifactCoordinates coordinates, String url @@ -84,6 +70,7 @@ record ArtifactIssuesUpdated( } @JsonDeserialize + @JsonTypeName("git-repo") record ArtifactGitRepositoryUpdated( ArtifactCoordinates coordinates, String gitRepo @@ -95,6 +82,7 @@ record ArtifactGitRepositoryUpdated( } @JsonDeserialize + @JsonTypeName("website") record ArtifactWebsiteUpdated( ArtifactCoordinates coordinates, String url 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 fd838b95..9f4253bc 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 @@ -25,20 +25,14 @@ package org.spongepowered.downloads.artifacts.events; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import java.io.Serial; - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(GroupUpdate.GroupRegistered.class), - @JsonSubTypes.Type(GroupUpdate.ArtifactRegistered.class), -}) -public interface GroupUpdate { +public sealed interface GroupUpdate extends AkkaSerializable { String groupId(); @@ -57,8 +51,6 @@ record GroupRegistered(String groupId, String name, String website) @JsonDeserialize final record ArtifactRegistered(ArtifactCoordinates coordinates) implements GroupUpdate { - @Serial private static final long serialVersionUID = 6319289932327553919L; - @JsonCreator public ArtifactRegistered { } diff --git a/artifacts/gradle.properties b/artifacts/gradle.properties deleted file mode 100644 index a9d280a0..00000000 --- a/artifacts/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -micronautVersion=3.8.2 diff --git a/artifacts/gradle/wrapper/gradle-wrapper.properties b/artifacts/gradle/wrapper/gradle-wrapper.properties index ae04661e..fae08049 100644 --- a/artifacts/gradle/wrapper/gradle-wrapper.properties +++ b/artifacts/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/artifacts/micronaut-cli.yml b/artifacts/micronaut-cli.yml deleted file mode 100644 index dc960e1f..00000000 --- a/artifacts/micronaut-cli.yml +++ /dev/null @@ -1,6 +0,0 @@ -applicationType: default -defaultPackage: org.spongepowered.downloads.artifacts -testFramework: junit -sourceLanguage: java -buildTool: gradle -features: [annotation-api, app-name, cache-caffeine, data, data-r2dbc, discovery-kubernetes, github-workflow-ci, graalvm, gradle, h2, http-client, java, java-application, junit, junit-params, kafka, logback, micronaut-aot, micronaut-build, netty-server, openapi, r2dbc, readme, security-annotations, security-jwt, security-ldap, serialization-jackson, shade, test-resources, toml, toml-build] diff --git a/artifacts/server/build.gradle.kts b/artifacts/server/build.gradle.kts index 21f64ea2..48d5dc38 100644 --- a/artifacts/server/build.gradle.kts +++ b/artifacts/server/build.gradle.kts @@ -1,79 +1,81 @@ +import io.micronaut.gradle.testresources.StartTestResourcesService +import io.micronaut.testresources.buildtools.KnownModules - -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project -val vavr: String by project +plugins { + id("io.micronaut.application") + id("io.micronaut.test-resources") + id("com.github.johnrengelman.shadow") +} -tasks { - dockerBuild { - images.add("${project.name}:${project.version}") - } - dockerBuildNative { - images.add("${project.name}:${project.version}") - - } -} -graalvmNative.toolchainDetection.set(false) micronaut { + runtime("netty") testRuntime("junit5") processing { incremental(true) - annotations("systemofadownload.*") + annotations("org.spongepowered.downloads.artifacts.*") } testResources { - additionalModules.add("r2dbc-postgresql") + enabled.set(true) + sharedServer.set(true) + additionalModules.addAll(KnownModules.R2DBC_POSTGRESQL) } } -graalvmNative { - binaries { - named("main") { - imageName.set("mn-graalvm-application") - buildArgs("--verboase") - } + +tasks { + test { + useJUnitPlatform() } } +tasks.withType().configureEach { + useClassDataSharing.set(false) +} + +graalvmNative.toolchainDetection.set(false) dependencies { implementation(project(":artifacts:api")) - implementation("io.vavr:vavr:${vavr}") + implementation(project(":artifacts:events")) + implementation(project(":akka")) + implementation(libs.vavr) annotationProcessor("io.micronaut.data:micronaut-data-processor") - annotationProcessor("io.micronaut:micronaut-http-validation") - annotationProcessor("io.micronaut.openapi:micronaut-openapi") - annotationProcessor("io.micronaut.security:micronaut-security-annotations") + annotationProcessor("io.micronaut.validation:micronaut-validation-processor") annotationProcessor("io.micronaut.serde:micronaut-serde-processor") - implementation("com.ongres.scram:client:2.1") - implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut:micronaut-jackson-databind") - implementation("io.micronaut.data:micronaut-data-r2dbc") - implementation("io.micronaut.liquibase:micronaut-liquibase") - implementation("io.micronaut.reactor:micronaut-reactor") - implementation("io.micronaut.reactor:micronaut-reactor-http-client") - implementation("io.micronaut.security:micronaut-security-ldap") implementation("io.micronaut.serde:micronaut-serde-jackson") - implementation("io.micronaut.toml:micronaut-toml") - implementation("io.micronaut.xml:micronaut-jackson-xml") - implementation("io.swagger.core.v3:swagger-annotations") + implementation("io.micronaut:micronaut-http-server-netty") + + runtimeOnly("org.yaml:snakeyaml") + + implementation(libs.bundles.appSerder) + implementation(libs.bundles.akkaManagement) + implementation(libs.bundles.actorsPersistence) + + // databases + implementation("io.micronaut.data:micronaut-data-r2dbc") +// implementation("io.micronaut.sql:micronaut-vertx-pg-client") +// implementation("io.micronaut.sql:micronaut-hibernate-reactive") implementation("io.vertx:vertx-pg-client") - implementation("jakarta.annotation:jakarta.annotation-api") - implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) - implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") - implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") - implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") - - runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly(libs.postgres.r2dbc) runtimeOnly("org.postgresql:postgresql") - runtimeOnly("org.postgresql:r2dbc-postgresql") + + + implementation("io.micronaut.liquibase:micronaut-liquibase") + implementation("io.micronaut:micronaut-http-client-jdk") + 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")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + testResourcesService("org.postgresql:postgresql") compileOnly("org.graalvm.nativeimage:svm") - implementation("io.micronaut:micronaut-validation") + + testImplementation("org.junit.jupiter:junit-jupiter-engine") + + +//// compileOnly("org.graalvm.nativeimage:svm") +// +// implementation("io.micronaut:micronaut-validation") } 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 96a9adb2..bd12b8ba 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 @@ -3,34 +3,23 @@ import akka.actor.typed.ActorSystem; import akka.actor.typed.SpawnProtocol; import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import io.micronaut.context.annotation.Bean; import io.micronaut.context.annotation.Factory; -import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.runtime.Micronaut; import io.micronaut.runtime.event.annotation.EventListener; import io.micronaut.runtime.server.event.ServerStartupEvent; -import io.swagger.v3.oas.annotations.*; -import io.swagger.v3.oas.annotations.info.*; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import org.spongepowered.downloads.artifacts.server.global.GlobalManager; -@OpenAPIDefinition( - info = @Info( - title = "artifacts", - version = "0.0" - ) -) @Singleton @Factory public class Application { - private final ActorSystem system; + private final ActorSystem system; private final ClusterSharding sharding; @Inject public Application( - final ActorSystem system, + final ActorSystem system, final ClusterSharding sharding ) { this.system = system; @@ -43,6 +32,5 @@ public static void main(String[] args) { @EventListener public void onApplicationEvent(final ServerStartupEvent event) { - } } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java index 9f014a26..f6462d9a 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/ArtifactDetailsEntity.java @@ -34,27 +34,23 @@ import akka.persistence.typed.javadsl.CommandHandlerWithReply; import akka.persistence.typed.javadsl.EventHandler; import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; -import com.lightbend.lagom.javadsl.api.transport.NotFound; -import com.lightbend.lagom.javadsl.persistence.AkkaTaggerAdapter; -import io.vavr.control.Either; +import io.micronaut.http.HttpResponse; import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; -import org.spongepowered.downloads.artifact.details.state.DetailsState; -import org.spongepowered.downloads.artifact.details.state.EmptyState; -import org.spongepowered.downloads.artifact.details.state.PopulatedState; +import org.spongepowered.downloads.artifacts.events.DetailsEvent; +import org.spongepowered.downloads.artifacts.server.details.state.DetailsState; +import org.spongepowered.downloads.artifacts.server.details.state.EmptyState; +import org.spongepowered.downloads.artifacts.server.details.state.PopulatedState; import 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 NotFound("group or artifact not found")); + + private static final HttpResponse NOT_FOUND = HttpResponse.notFound(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 final Function> tagger; private ArtifactDetailsEntity( ActorContext ctx, @@ -64,7 +60,6 @@ private ArtifactDetailsEntity( super(persistenceId); this.artifactId = entityId; this.ctx = ctx; - this.tagger = AkkaTaggerAdapter.fromLagom(context, DetailsEvent.TAG); } public static Behavior create( @@ -140,15 +135,13 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.validUrl().toString())) .thenReply( cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) + us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + )) ) ) .onCommand( @@ -157,15 +150,13 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.website().toString())) .thenReply( cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) + us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + )) ) ) .onCommand( @@ -174,15 +165,13 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactIssuesUpdated(s.coordinates(), cmd.displayName())) .thenReply( cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) + us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + )) ) ) .onCommand( @@ -191,23 +180,17 @@ public CommandHandlerWithReply comma .persist(new DetailsEvent.ArtifactGitRepositoryUpdated(s.coordinates(), cmd.gitRemote())) .thenReply( cmd.replyTo(), - us -> Either.right( - new ArtifactDetails.Response( - us.coordinates().artifactId(), - us.displayName(), - us.website(), - us.issues(), - us.gitRepository() - ) - ) + us -> HttpResponse.ok(new ArtifactDetails.Response.Ok( + us.coordinates().artifactId(), + us.displayName(), + us.website(), + us.issues(), + us.gitRepository() + )) ) ); return builder.build(); } - @Override - public Set tagsFor(final DetailsEvent detailsEvent) { - return this.tagger.apply(detailsEvent); - } } diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java index bcb8d442..04027110 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsCommand.java @@ -32,6 +32,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.micronaut.http.HttpResponse; import io.vavr.control.Either; +import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; @@ -52,7 +53,7 @@ @JsonSubTypes.Type(value = DetailsCommand.UpdateDisplayName.class, name = "display-name") }) -public interface DetailsCommand { +public interface DetailsCommand extends AkkaSerializable { @JsonDeserialize final record RegisterArtifact(ArtifactCoordinates coordinates, diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java deleted file mode 100644 index e851dbca..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/DetailsManager.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.details; - -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.http.HttpResponse; -import io.vavr.control.Either; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.api.errors.InvalidRemoteException; -import org.eclipse.jgit.lib.Ref; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.query.ArtifactDetails; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; - -import java.net.URL; -import java.time.Duration; -import java.util.Collection; -import java.util.concurrent.CompletionStage; - -@Singleton -public class DetailsManager { - private final ClusterSharding clusterSharding; - private final Duration askTimeout = Duration.ofHours(5); - - @Inject - public DetailsManager(final ClusterSharding clusterSharding) { - this.clusterSharding = clusterSharding; - this.clusterSharding.init( - Entity.of( - ArtifactDetailsEntity.ENTITY_TYPE_KEY, - context -> ArtifactDetailsEntity.create( - context, - context.getEntityId(), - PersistenceId.of(context.getEntityTypeKey().name(), context.getEntityId()) - ) - ) - ); - } - - public CompletionStage UpdateWebsite( - ArtifactCoordinates coords, ArtifactDetails.Update.Website w - ) { - - final Either validate = w.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var validUrl = validate.get(); - return this.getDetailsEntity(coords) - .>ask( - r -> new DetailsCommand.UpdateWebsite(coords, validUrl, r), this.askTimeout) - .thenApply(Either::get); - } - - - public CompletionStage UpdateDisplayName( - ArtifactCoordinates coords, ArtifactDetails.Update.DisplayName d - ) { - final Either validate = d.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var displayName = validate.get(); - return this.getDetailsEntity(coords) - .>ask( - r -> new DetailsCommand.UpdateDisplayName(coords, displayName, r), - this.askTimeout - ) - .thenApply(Either::get); - } - - public CompletionStage UpdateGitRepository( - final ArtifactCoordinates coords, ArtifactDetails.Update.GitRepository gr - ) { - final Either validate = gr.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final Collection refs; - try { - refs = Git.lsRemoteRepository() - .setRemote(gr.gitRepo()) - .call(); - } catch (InvalidRemoteException e) { - throw new BadRequest(String.format("Invalid remote: %s", gr.gitRepo())); - } catch (GitAPIException e) { - throw new BadRequest(String.format("Error resolving repository '%s'", gr.gitRepo())); - } - if (refs.isEmpty()) { - throw new BadRequest(String.format("Remote repository '%s' has no refs", gr.gitRepo())); - } - - return this.getDetailsEntity(coords) - .>ask( - r -> new DetailsCommand.UpdateGitRepository(coords, gr.gitRepo(), r), this.askTimeout) - .thenApply(Either::get); - } - - public CompletionStage UpdateIssues( - ArtifactCoordinates coords, ArtifactDetails.Update.Issues i - ) { - final Either validate = i.validate(); - if (validate.isLeft()) { - throw validate.getLeft(); - } - final var validUrl = validate.get(); - return this.getDetailsEntity(coords) - .>ask( - r -> new DetailsCommand.UpdateIssues(coords, validUrl, r), this.askTimeout).thenApply( - Either::get); - } - - private EntityRef getDetailsEntity(final ArtifactCoordinates coordinates) { - return this.getDetailsEntity(coordinates.groupId(), coordinates.artifactId()); - } - - private EntityRef getDetailsEntity(final String groupId, final String artifactId) { - return this.clusterSharding.entityRefFor(ArtifactDetailsEntity.ENTITY_TYPE_KEY, groupId + ":" + artifactId); - } - - public CompletionStage registerArtifact( - ArtifactRegistration.Response.ArtifactRegistered registered, - String displayName - ) { - return this.getDetailsEntity(registered.coordinates()) - .ask( - replyTo -> new DetailsCommand.RegisterArtifact( - registered.coordinates(), displayName, replyTo), this.askTimeout) - .thenApply(notUsed -> registered); - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java index 5695e90f..15d01fdd 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/DetailsState.java @@ -24,10 +24,12 @@ */ package org.spongepowered.downloads.artifacts.server.details.state; -import com.lightbend.lagom.serialization.Jsonable; +import org.spongepowered.downloads.akka.AkkaSerializable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -public interface DetailsState extends Jsonable { +public sealed interface DetailsState extends AkkaSerializable + permits EmptyState, PopulatedState { + ArtifactCoordinates coordinates(); String displayName(); diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java index e120427d..10cf2b76 100644 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/details/state/PopulatedState.java @@ -26,14 +26,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.CompressedJsonable; import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.details.DetailsEvent; +import org.spongepowered.downloads.artifacts.events.DetailsEvent; @JsonDeserialize public record PopulatedState(ArtifactCoordinates coordinates, String displayName, String website, String gitRepository, - String issues) implements DetailsState, CompressedJsonable { + String issues) implements DetailsState { @JsonCreator public PopulatedState { diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java deleted file mode 100644 index 1fcaf252..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalCommand.java +++ /dev/null @@ -1,62 +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.global; - -import akka.Done; -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 com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.Group; -import org.spongepowered.downloads.artifact.api.query.GroupsResponse; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(name = "Get", - value = GlobalCommand.GetGroups.class) -}) -public interface GlobalCommand extends Jsonable { - - @JsonDeserialize - record GetGroups(ActorRef replyTo) - implements GlobalCommand { - - @JsonCreator - public GetGroups { - } - } - - @JsonDeserialize - record RegisterGroup( - ActorRef replyTo, Group group) implements GlobalCommand { - - @JsonCreator - public RegisterGroup { - } - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.java deleted file mode 100644 index d9162e8f..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalEvent.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.global; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.Group; - -public interface GlobalEvent extends AggregateEvent, Jsonable { - - AggregateEventTag TAG = AggregateEventTag.of(GlobalEvent.class); - - @Override - default AggregateEventTagger aggregateTag() { - return TAG; - } - - @JsonDeserialize - final class GroupRegistered implements GlobalEvent { - public final Group group; - - @JsonCreator - public GroupRegistered(Group group) { - this.group = group; - } - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java deleted file mode 100644 index 907ad2b0..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalManager.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.global; - -import akka.Done; -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 jakarta.inject.Inject; -import jakarta.inject.Singleton; -import org.spongepowered.downloads.artifact.api.Group; -import org.spongepowered.downloads.artifact.api.query.GroupRegistration; -import org.spongepowered.downloads.artifact.api.query.GroupsResponse; - -import java.time.Duration; -import java.util.concurrent.CompletionStage; - -@Singleton -public final class GlobalManager { - private final Duration askTimeout = Duration.ofHours(5); - - private final ClusterSharding clusterSharding; - - @Inject - public GlobalManager(final ClusterSharding clusterSharding) { - this.clusterSharding = clusterSharding; - this.clusterSharding.init( - Entity.of( - GlobalRegistration.ENTITY_TYPE_KEY, - ctx -> GlobalRegistration.create( - ctx.getEntityId(), - PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()) - ) - ) - ); - } - - public CompletionStage registerGroup( - GroupRegistration.Response.GroupRegistered registered - ) { - final Group group = registered.group(); - return this.getGlobalEntity() - .ask(replyTo -> new GlobalCommand.RegisterGroup(replyTo, group), this.askTimeout) - .thenApply(notUsed -> registered); - } - - - private EntityRef getGlobalEntity() { - return this.clusterSharding.entityRefFor(GlobalRegistration.ENTITY_TYPE_KEY, "global"); - } - - public CompletionStage getGroups() { - return this.getGlobalEntity().ask(GlobalCommand.GetGroups::new, this.askTimeout); - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java deleted file mode 100644 index 52d20c76..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalRegistration.java +++ /dev/null @@ -1,99 +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.global; - -import akka.Done; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -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 akka.persistence.typed.javadsl.ReplyEffect; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.query.GroupsResponse; - -public class GlobalRegistration - extends EventSourcedBehaviorWithEnforcedReplies { - - public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( - GlobalCommand.class, "GlobalEntity"); - private final String groupId; - private final ActorContext ctx; - - private GlobalRegistration(ActorContext ctx, String entityId, PersistenceId persistenceId) { - super(persistenceId); - this.ctx = ctx; - this.groupId = entityId; - } - - public static Behavior create(String entityId, PersistenceId persistenceId) { - return Behaviors.setup(ctx -> new GlobalRegistration(ctx, entityId, persistenceId)); - } - - @Override - public GlobalState emptyState() { - return new GlobalState(List.empty()); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - builder.forAnyState() - .onEvent( - GlobalEvent.GroupRegistered.class, - (state, event) -> new GlobalState(state.groups().append(event.group)) - ); - return builder.build(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - builder.forAnyState() - .onCommand( - GlobalCommand.GetGroups.class, - (state, cmd) -> this.Effect().reply(cmd.replyTo(), new GroupsResponse.Available(state.groups())) - ) - .onCommand( - GlobalCommand.RegisterGroup.class, - this::handleRegisterGroup - ); - return builder.build(); - } - - private ReplyEffect handleRegisterGroup( - GlobalState state, GlobalCommand.RegisterGroup cmd - ) { - if (!state.groups().contains(cmd.group())) { - return this.Effect().persist(new GlobalEvent.GroupRegistered(cmd.group())) - .thenReply(cmd.replyTo(), (s) -> Done.done()); - } - return this.Effect().reply(cmd.replyTo(), Done.done()); - } - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java deleted file mode 100644 index 9a26c915..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/global/GlobalState.java +++ /dev/null @@ -1,38 +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.global; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.Group; - -@JsonDeserialize -public record GlobalState(List groups) { - - @JsonCreator - public GlobalState { - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java deleted file mode 100644 index 08c03a79..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupCommand.java +++ /dev/null @@ -1,63 +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.groups; - -import akka.actor.typed.ActorRef; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; -import org.spongepowered.downloads.artifact.api.query.GroupRegistration; -import org.spongepowered.downloads.artifact.api.query.GroupResponse; - -public sealed interface GroupCommand { - record GetGroup( - String groupId, - ActorRef replyTo - ) implements GroupCommand { - } - - record GetArtifacts( - String groupId, - ActorRef replyTo - ) implements GroupCommand { - - } - - record RegisterArtifact( - String artifact, - ActorRef replyTo - ) implements GroupCommand { - - } - - record RegisterGroup( - String mavenCoordinates, - String name, - String website, - ActorRef replyTo - ) implements GroupCommand { - - } - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.java deleted file mode 100644 index d2fa9f83..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEntity.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.groups; - -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.control.Try; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.Group; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -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.artifacts.server.groups.state.EmptyState; -import org.spongepowered.downloads.artifacts.server.groups.state.GroupState; -import org.spongepowered.downloads.artifacts.server.groups.state.PopulatedState; - -import java.net.URL; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class GroupEntity - extends EventSourcedBehaviorWithEnforcedReplies { - - public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create(GroupCommand.class, "GroupEntity"); - private final String groupId; - private final Function> tagger; - - 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(); - this.tagger = AkkaTaggerAdapter.fromLagom(context, GroupEvent.TAG); - - } - - 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( - GroupEvent.GroupRegistered.class, - this::handleRegistration - ); - builder.forStateType(PopulatedState.class) - .onEvent(GroupEvent.ArtifactRegistered.class, this::handleArtifactRegistration); - - return builder.build(); - } - - private GroupState handleRegistration( - final GroupState state, final GroupEvent.GroupRegistered event - ) { - return new PopulatedState(event.groupId, event.name, event.website, Set.of()); - } - - private GroupState handleArtifactRegistration( - final PopulatedState state, final GroupEvent.ArtifactRegistered event - ) { - final var add = Stream.concat( - state.artifacts().stream(), - Stream.of(event.artifact()) - ) - .collect(Collectors.toUnmodifiableSet()); - 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 ArtifactRegistration.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); - return builder.build(); - } - - @Override - public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(5, 2); - } - - @Override - public Set tagsFor(final GroupEvent groupEvent) { - return this.tagger.apply(groupEvent); - } - - private ReplyEffect respondToRegisterGroup( - final GroupState state, - final GroupCommand.RegisterGroup cmd - ) { - return this.Effect() - .persist(new GroupEvent.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 ArtifactRegistration.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 GroupEvent.ArtifactRegistered(state.groupCoordinates(), cmd.artifact())) - .thenReply(cmd.replyTo(), (s) -> new ArtifactRegistration.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(() -> new URL(website)) - .mapTry(url -> { - final Group group = new Group(state.groupCoordinates(), state.name(), website); - return new GroupResponse.Available(group); - }) - .getOrElseGet(throwable -> 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().stream().toList())); - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java deleted file mode 100644 index 01fdb996..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupEvent.java +++ /dev/null @@ -1,124 +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.groups; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -import java.io.Serial; -import java.util.Objects; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(GroupEvent.GroupRegistered.class), - @JsonSubTypes.Type(GroupEvent.ArtifactRegistered.class), -}) -public interface GroupEvent extends AggregateEvent, Jsonable { - - AggregateEventShards TAG = AggregateEventTag.sharded(GroupEvent.class, 10); - - @Override - default AggregateEventTagger aggregateTag() { - return TAG; - } - - String groupId(); - - @JsonTypeName("group-registered") - @JsonDeserialize - final class GroupRegistered implements GroupEvent { - @Serial private static final long serialVersionUID = 0L; - - public final String groupId; - public final String name; - public final String website; - - @JsonCreator - public GroupRegistered(final String groupId, final String name, final String website) { - this.groupId = groupId; - this.name = name; - this.website = website; - } - - @Override - public String groupId() { - return this.groupId; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (GroupRegistered) obj; - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.name, that.name) && - Objects.equals(this.website, that.website); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.name, this.website); - } - - @Override - public String toString() { - return "GroupRegistered[" + - "groupId=" + this.groupId + ", " + - "name=" + this.name + ", " + - "website=" + this.website + ']'; - } - - } - - @JsonTypeName("artifact-registered") - @JsonDeserialize - record ArtifactRegistered( - String groupId, - String artifact - ) implements GroupEvent { - - public ArtifactCoordinates coordinates() { - return new ArtifactCoordinates(this.groupId, this.artifact); - } - - @JsonCreator - public ArtifactRegistered { - } - } - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java deleted file mode 100644 index 7783d3cf..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupManager.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.groups; - -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import akka.cluster.sharding.typed.javadsl.EntityRef; -import com.lightbend.lagom.javadsl.api.transport.NotFound; -import jakarta.inject.Inject; -import jakarta.inject.Singleton; -import org.spongepowered.downloads.artifact.api.query.ArtifactRegistration; -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.artifacts.server.details.DetailsManager; -import org.spongepowered.downloads.artifacts.server.global.GlobalManager; - -import java.time.Duration; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Singleton -public final class GroupManager { - private final ClusterSharding clusterSharding; - private final GlobalManager global; - private final Duration askTimeout = Duration.ofHours(5); - private final DetailsManager details; - - @Inject - public GroupManager(ClusterSharding clusterSharding, final DetailsManager details, final GlobalManager global) { - this.clusterSharding = clusterSharding; - this.global = global; - this.details = details; - this.clusterSharding.init( - Entity.of( - GroupEntity.ENTITY_TYPE_KEY, - GroupEntity::create - ) - ); - } - - public CompletionStage registerGroup( - GroupRegistration.RegisterGroupRequest registration - ) { - final String mavenCoordinates = registration.groupCoordinates(); - final String name = registration.name(); - final String website = registration.website(); - return this.groupEntity(registration.groupCoordinates().toLowerCase(Locale.ROOT)) - .ask( - replyTo -> new GroupCommand.RegisterGroup(mavenCoordinates, name, website, replyTo), - this.askTimeout - ).thenCompose(response -> { - if (!(response instanceof GroupRegistration.Response.GroupRegistered registered)) { - return CompletableFuture.completedFuture(response); - } - return this.global.registerGroup(registered); - - }); - } - - - private EntityRef groupEntity(final String groupId) { - return this.clusterSharding.entityRefFor(GroupEntity.ENTITY_TYPE_KEY, groupId.toLowerCase(Locale.ROOT)); - } - - public CompletionStage registerArtifact( - ArtifactRegistration.RegisterArtifact reg, String groupId - ) { - final var sanitizedGroupId = groupId.toLowerCase(Locale.ROOT); - return this.groupEntity(sanitizedGroupId) - .ask( - replyTo -> new GroupCommand.RegisterArtifact(reg.artifactId(), replyTo), this.askTimeout) - .thenCompose(response -> switch (response) { - case ArtifactRegistration.Response.GroupMissing missing -> - throw new NotFound(String.format("group %s does not exist", missing.s())); - - case ArtifactRegistration.Response.ArtifactRegistered registered -> - this.details.registerArtifact(registered, reg.displayName()); - - default -> CompletableFuture.completedFuture(response); - }); - } - - public CompletionStage getArtifacts(String groupId) { - return this.groupEntity(groupId) - .ask(replyTo -> new GroupCommand.GetArtifacts(groupId, replyTo), this.askTimeout) - .thenApply(response -> switch (response) { - case GetArtifactsResponse.GroupMissing m -> throw new NotFound(String.format("group '%s' not found", m.groupRequested())); - case GetArtifactsResponse.ArtifactsAvailable a -> a; - }); - } - - public CompletionStage get(String groupId) { - return this.groupEntity(groupId.toLowerCase(Locale.ROOT)) - .ask(replyTo -> new GroupCommand.GetGroup(groupId, replyTo), this.askTimeout) - .thenApply(response -> switch (response) { - case GroupResponse.Missing m -> throw new NotFound(String.format("group '%s' not found", m.groupId())); - case GroupResponse.Available a -> a; - }); - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java deleted file mode 100644 index 98ebb288..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/GroupsQueryController.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.groups; - -import akka.actor.typed.ActorSystem; -import akka.actor.typed.SpawnProtocol; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import akka.cluster.sharding.typed.javadsl.EntityRef; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import jakarta.inject.Inject; -import org.spongepowered.downloads.artifact.api.query.GroupRegistration; -import org.spongepowered.downloads.artifact.api.query.GroupResponse; - -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -@Controller("/groups") -public class GroupsQueryController { - - private final ActorSystem system; - private final ClusterSharding sharding; - - @Inject - public GroupsQueryController( - final ActorSystem system, - ClusterSharding sharding - ) { - this.system = system; - this.sharding = sharding; - this.sharding.init( - Entity.of( - GroupEntity.ENTITY_TYPE_KEY, - GroupEntity::create - ) - ); - - } - - @Post("/") - public CompletableFuture> registerGroup( - @Body GroupRegistration.RegisterGroupRequest req - ) { - final var ref = this.sharding.entityRefFor( - GroupEntity.ENTITY_TYPE_KEY, - req.groupCoordinates() - ); - final var resp = ref.ask - ( - replyTo -> new GroupCommand.RegisterGroup( - req.groupCoordinates(), - req.name(), - req.website(), - replyTo - ), - Duration.ofSeconds(10) - ); - return resp - .>thenApply(HttpResponse::created) - .toCompletableFuture(); - } -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java deleted file mode 100644 index e6b3de5a..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/EmptyState.java +++ /dev/null @@ -1,62 +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.groups.state; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.artifact.api.Group; - -@JsonDeserialize -public record EmptyState() implements GroupState { - - @JsonCreator - public EmptyState { - } - - @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/groups/state/GroupState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java deleted file mode 100644 index 8b133996..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/GroupState.java +++ /dev/null @@ -1,47 +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.groups.state; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.artifact.api.Group; - -@JsonDeserialize -@JsonSubTypes({ - @JsonSubTypes.Type(value = PopulatedState.class, name = "populated"), - @JsonSubTypes.Type(value = EmptyState.class, name = "empty") -}) -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/groups/state/PopulatedState.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java deleted file mode 100644 index 56264f25..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/groups/state/PopulatedState.java +++ /dev/null @@ -1,51 +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.groups.state; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.artifact.api.Group; - -import java.util.Set; - -@JsonDeserialize -public record PopulatedState( - String groupCoordinates, - String name, - String website, - Set artifacts -) implements GroupState { - @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/query/ArtifactsQuery.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java deleted file mode 100644 index fb46c84f..00000000 --- a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.query; - -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; - -@Controller("/groups/{groupID}/artifacts") -public class ArtifactsQuery { - - @Get("") - - -} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java new file mode 100644 index 00000000..ed8de22f --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/group/domain/GroupOrg.java @@ -0,0 +1,27 @@ +package org.spongepowered.downloads.artifacts.server.query.group.domain; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.MappedProperty; +import io.micronaut.data.annotation.Relation; +import io.micronaut.serde.annotation.Serdeable; +import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; + +import java.util.List; + +@MappedEntity(value = "groups") +@Serdeable +public class GroupOrg { + + @Id + @GeneratedValue + private int id; + + @MappedProperty(value = "groupId") + private String groupId; + + @Relation(value =Relation.Kind.ONE_TO_MANY, mappedBy = "groupId") + private List artifacts; + +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java new file mode 100644 index 00000000..4ebb194d --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactDto.java @@ -0,0 +1,31 @@ +package org.spongepowered.downloads.artifacts.server.query.meta; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +import java.io.Serializable; +import java.util.Set; + +/** + * DTO for {@link org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact} + */ +public record ArtifactDto( + @NotNull String groupId, + @NotEmpty String artifactId, + String displayName, + String website, + String gitRepo, + String issues, + @NotNull Set tagValues +) implements Serializable { + /** + * DTO for {@link org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifactTagValue} + */ + public record Tag( + String artifactId, + String groupId, + String tagName, + String tagValue + ) implements Serializable { + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java new file mode 100644 index 00000000..87c90b5b --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactQueryController.java @@ -0,0 +1,52 @@ +package org.spongepowered.downloads.artifacts.server.query.meta; + +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; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Get; +import io.micronaut.http.annotation.PathVariable; +import io.micronaut.http.annotation.Status; +import jakarta.inject.Inject; +import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Controller("/groups/{groupID}/artifacts") +@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; + } + + @Get(value = "/{artifactId}", + produces = MediaType.APPLICATION_JSON + ) + @Status(HttpStatus.OK) + public Mono getArtifacts( + final @PathVariable String groupID, + final @PathVariable String artifactId + ) { + return this.artifactsRepo.findByGroupIdAndArtifactId(groupID, artifactId) + .map(a -> new GetArtifactsResponse.ArtifactsAvailable(List.of(a.getArtifactId()))) + .onErrorReturn(new GetArtifactsResponse.GroupMissing(groupID)); + } +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java new file mode 100644 index 00000000..5032a032 --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/ArtifactRepository.java @@ -0,0 +1,22 @@ +package org.spongepowered.downloads.artifacts.server.query.meta; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.data.annotation.Query; +import io.micronaut.data.model.query.builder.sql.Dialect; +import io.micronaut.data.r2dbc.annotation.R2dbcRepository; +import io.micronaut.data.repository.reactive.ReactiveStreamsCrudRepository; +import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; + +@R2dbcRepository(dialect = Dialect.POSTGRES) +public interface ArtifactRepository extends ReactiveStreamsCrudRepository { + + @NonNull + List findArtifactIdByGroupId(@NonNull String groupId); + + @NonNull + Mono findByGroupIdAndArtifactId(String groupId, String artifactId); +} diff --git a/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java new file mode 100644 index 00000000..05741eba --- /dev/null +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/Group.java @@ -0,0 +1,27 @@ +package org.spongepowered.downloads.artifacts.server.query.meta.domain; + +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.MappedProperty; +import io.micronaut.data.annotation.Relation; +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; + +@Serdeable +@MappedEntity +public class Group { + + @GeneratedValue + @Id + private Long id; + + @MappedProperty(value = "groupId") + private String groupId; + + @Relation(value = Relation.Kind.ONE_TO_MANY, + mappedBy = "groupId") + private List artifacts; + +} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java similarity index 54% rename from downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java index 0ee0309d..27c5456a 100644 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifact.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifact.java @@ -22,94 +22,59 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.models; +package org.spongepowered.downloads.artifacts.server.query.meta.domain; +import io.micronaut.data.annotation.GeneratedValue; +import io.micronaut.data.annotation.Id; +import io.micronaut.data.annotation.Join; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.MappedProperty; +import io.micronaut.data.annotation.Relation; import io.vavr.Tuple; import io.vavr.Tuple2; import io.vavr.collection.Map; import io.vavr.collection.SortedSet; import io.vavr.collection.TreeMap; import io.vavr.collection.TreeSet; +import jakarta.validation.constraints.NotEmpty; import org.apache.maven.artifact.versioning.ComparableVersion; -import org.hibernate.annotations.Immutable; -import org.spongepowered.downloads.api.ArtifactCoordinates; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import java.io.Serializable; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + import java.util.Comparator; -import java.util.Objects; import java.util.Set; -import java.util.stream.Collectors; - -@Immutable -@Entity(name = "Artifact") -@Table(name = "artifacts", - schema = "version") -@NamedQueries({ - @NamedQuery( - name = "Artifact.findByCoordinates", - query = "select a from Artifact a where a.groupId = :groupId and a.artifactId = :artifactId" - ) -}) -public class JpaArtifact implements Serializable { + +@MappedEntity(value = "artifacts", schema = "artifacts", alias = "artifact") +public class JpaArtifact { + + @GeneratedValue @Id - @Column(name = "id", - nullable = false, - updatable = false, - insertable = false) private int id; - @Column(name = "group_id", - nullable = false, - updatable = false, - insertable = false) + public int getId() { + return id; + } + + @MappedProperty(value = "group_id") private String groupId; - @Column(name = "artifact_id", - nullable = false, - updatable = false, - insertable = false) + @NotEmpty + @MappedProperty(value = "artifact_id") private String artifactId; - @Column(name = "display_name", - updatable = false, - insertable = false) + @MappedProperty(value = "display_name") private String displayName; - @Column(name = "website", - updatable = false, - insertable = false) + @MappedProperty(value = "website") private String website; - @Column(name = "git_repository", - updatable = false, - insertable = false) + @MappedProperty(value = "git_repository") private String gitRepo; - @Column(name = "issues", - updatable = false, - insertable = false) + @MappedProperty(value = "issues") private String issues; - @OneToMany(fetch = FetchType.EAGER, - targetEntity = JpaArtifactTagValue.class) - @JoinColumns({ - @JoinColumn(name = "artifact_id", - referencedColumnName = "artifact_id"), - @JoinColumn(name = "group_id", - referencedColumnName = "group_id") - }) private Set tagValues; public String getGroupId() { @@ -140,26 +105,6 @@ public Set getTagValues() { return tagValues; } - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaArtifact that = (JpaArtifact) o; - return id == that.id && groupId.equals(that.groupId) && artifactId.equals( - that.artifactId) && Objects.equals(displayName, that.displayName) && Objects.equals( - website, that.website) && Objects.equals(gitRepo, that.gitRepo) && Objects.equals( - issues, that.issues); - } - - @Override - public int hashCode() { - return Objects.hash(id, groupId, artifactId, displayName, website, gitRepo, issues); - } - public ArtifactCoordinates getCoordinates() { return new ArtifactCoordinates(this.groupId, this.artifactId); } @@ -178,4 +123,5 @@ public Map> getTagValuesForReply() { } return versionedTags; } + } diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java similarity index 73% rename from downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java rename to artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java index d6a92e3d..9411baf9 100644 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/models/JpaArtifactTagValue.java +++ b/artifacts/server/src/main/java/org/spongepowered/downloads/artifacts/server/query/meta/domain/JpaArtifactTagValue.java @@ -22,27 +22,15 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifacts.models; +package org.spongepowered.downloads.artifacts.server.query.meta.domain; -import org.hibernate.annotations.Immutable; +import io.micronaut.data.annotation.MappedEntity; +import io.micronaut.data.annotation.MappedProperty; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.IdClass; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.ManyToOne; -import javax.persistence.Table; import java.io.Serializable; import java.util.Objects; -@Immutable -@Entity(name = "ArtifactTagValue") -@Table(name = "artifact_tag_values", - schema = "version") -@IdClass(JpaArtifactTagValue.Identifier.class) +@MappedEntity(value = "versioned_tags", schema = "version") public class JpaArtifactTagValue { /* @@ -78,38 +66,23 @@ public int hashCode() { } } - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "artifact_id", - referencedColumnName = "artifact_id"), - @JoinColumn(name = "group_id", - referencedColumnName = "group_id") - }) private JpaArtifact artifact; - @Id - @Column(name = "artifact_id", - insertable = false, - updatable = false) + @MappedProperty(value = "artifact_id") private String artifactId; - @Id - @Column(name = "group_id", - insertable = false, - updatable = false) + @MappedProperty(value = "group_id") private String groupId; - @Id - @Column(name = "tag_name", - insertable = false, - updatable = false) + @MappedProperty(value = "tag_name") private String tagName; - @Id - @Column(name = "tag_value", - insertable = false, - updatable = false) + @MappedProperty(value = "tag_value") private String tagValue; + public JpaArtifact getArtifact() { + return artifact; + } + public String getTagName() { return tagName; } diff --git a/artifacts/server/src/main/resources/application.conf b/artifacts/server/src/main/resources/application.conf new file mode 100644 index 00000000..997ab44f --- /dev/null +++ b/artifacts/server/src/main/resources/application.conf @@ -0,0 +1,32 @@ +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 { + journal.payload-column-type = JSONB + snapshot.payload-column-type = JSONB + state.payload-column-type = JSONB +} +akka.serialization.jackson.jackson-json.compression.algorithm = off + + +akka.persistence.r2dbc { + dialect = "postgres" + connection-factory { + driver = "postgres" + 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.toml b/artifacts/server/src/main/resources/application.toml deleted file mode 100644 index 250d1b57..00000000 --- a/artifacts/server/src/main/resources/application.toml +++ /dev/null @@ -1,10 +0,0 @@ -micronaut.application.name = 'artifacts' -netty.default.allocator.max-order = 3 - -[r2dbc.datasources.default] -schema-generate = 'CREATE_DROP' -dialect = 'H2' - -[micronaut.security] -authentication = 'bearer' -token.jwt.signatures.secret.generator.secret = '${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}' diff --git a/artifacts/server/src/main/resources/application.yaml b/artifacts/server/src/main/resources/application.yaml new file mode 100644 index 00000000..754ed774 --- /dev/null +++ b/artifacts/server/src/main/resources/application.yaml @@ -0,0 +1,24 @@ +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) 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 new file mode 100644 index 00000000..b6adedcb --- /dev/null +++ b/artifacts/server/src/main/resources/db/changelog/01-create-artifacts-schema.xml @@ -0,0 +1,125 @@ + + + + + + + + CREATE SCHEMA IF NOT EXISTS artifacts; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select distinct a.artifact_id, a.group_id, v.version, v.recommended, v.manual_recommendation + from artifact.artifacts a inner join artifact.artifact_versions v on a.id = v.artifact_id + + + select + a.group_id, + a.artifact_id, + av.version, + va.classifier, va.extension, va.download_url, va.md5, va.sha1 + from artifact.versioned_assets va + inner join artifact.artifact_versions av on av.id = va.version_id + inner join artifact.artifacts a on a.id = av.artifact_id + + + select distinct a.group_id, a.artifact_id, v.version, v.ordering, v.id as version_id, vc.commit_sha, vc.repo, vc.branch, vc.changelog + from artifact.version_changelogs vc + inner join artifact.artifact_versions v on v.id = vc.version_id + inner join artifact.artifacts a on a.id = v.artifact_id + order by v.ordering desc + + + select distinct a.artifact_id, a.group_id, v.version, v.ordering, v.recommended, v.manual_recommendation + from artifact.artifacts a inner join artifact.artifact_versions v on a.id = v.artifact_id + order by v.ordering desc + + + + diff --git a/artifacts/server/src/main/resources/db/liquibase-changelog.xml b/artifacts/server/src/main/resources/db/liquibase-changelog.xml new file mode 100644 index 00000000..92a9682b --- /dev/null +++ b/artifacts/server/src/main/resources/db/liquibase-changelog.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/artifacts/server/src/main/resources/logback.xml b/artifacts/server/src/main/resources/logback.xml index 6010eb52..8cb499db 100644 --- a/artifacts/server/src/main/resources/logback.xml +++ b/artifacts/server/src/main/resources/logback.xml @@ -12,4 +12,9 @@ + + + + + diff --git a/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java new file mode 100644 index 00000000..8a5c796d --- /dev/null +++ b/artifacts/server/src/test/java/org/spongepowered/downloads/test/artifacts/server/ArtifactRepositoryTest.java @@ -0,0 +1,118 @@ +package org.spongepowered.downloads.test.artifacts.server; + +import io.micronaut.context.BeanContext; +import io.micronaut.core.type.Argument; +import io.micronaut.data.annotation.Query; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ExecutableMethod; +import io.micronaut.runtime.EmbeddedApplication; +import io.micronaut.serde.annotation.Serdeable; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import io.micronaut.test.extensions.junit5.annotation.TestResourcesScope; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.spongepowered.downloads.artifacts.server.query.meta.ArtifactRepository; +import org.spongepowered.downloads.artifacts.server.query.meta.domain.JpaArtifact; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Optional; + + +@MicronautTest +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestResourcesScope("testcontainers") +public class ArtifactRepositoryTest { + @Inject + BeanContext context; + + @Inject + EmbeddedApplication application; + + @Inject + @Client("/") + HttpClient httpClient; + + @Test + public void testItWorks() { + Assertions.assertTrue(application.isRunning()); + } + + @Test + void migrationsAreExposedViaAndEndpoint() { + BlockingHttpClient client = httpClient.toBlocking(); + + HttpResponse> response = client.exchange( + HttpRequest.GET("/liquibase"), + Argument.listOf(LiquibaseReport.class) + ); + Assertions.assertEquals(HttpStatus.OK, response.status()); + + LiquibaseReport liquibaseReport = response.body().get(0); + Assertions.assertNotNull(liquibaseReport); + Assertions.assertNotNull(liquibaseReport.getChangeSets()); + Assertions.assertEquals(2, liquibaseReport.getChangeSets().size()); + } + @Serdeable + static class LiquibaseReport { + + private List changeSets; + + public void setChangeSets(List changeSets) { + this.changeSets = changeSets; + } + + public List getChangeSets() { + return changeSets; + } + } + + @Serdeable + static class ChangeSet { + + private String id; + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + } + + @Test + public void testAnnotation() { + final BeanDefinition beanDefinition = context.getBeanDefinition(ArtifactRepository.class); + final ExecutableMethod findByGroupIdAndArtifactId = beanDefinition // (1) + .getRequiredMethod("findByGroupIdAndArtifactId", String.class, String.class); + String query = findByGroupIdAndArtifactId // (2) + .getAnnotationMetadata().stringValue(Query.class) // (3) + .orElse(null); + + final String expected = "SELECT artifact.\"id\",artifact.\"group_id\",artifact.\"artifact_id\",artifact.\"display_name\",artifact.\"website\",artifact.\"git_repository\",artifact.\"issues\",artifact.\"tag_values\",artifact.\"coordinates\",artifact.\"tag_values_for_reply\" FROM \"artifacts\".\"artifacts\" artifact WHERE (artifact.\"group_id\" = $1 AND artifact.\"artifact_id\" = $2)"; + Assertions.assertEquals( // (4) + expected, query); + } + + @Test + public void testGetArtifact() { + final ArtifactRepository repo = context.createBean(ArtifactRepository.class); + + + final Mono spongevanilla = repo.findByGroupIdAndArtifactId("org.spongepowered", "spongevanilla"); + final JpaArtifact a = spongevanilla.block(); + + } + + + +} diff --git a/artifacts/server/src/test/resources/application-test.conf b/artifacts/server/src/test/resources/application-test.conf new file mode 100644 index 00000000..a4090ab5 --- /dev/null +++ b/artifacts/server/src/test/resources/application-test.conf @@ -0,0 +1,10 @@ + +akka { + actor { + provider = "cluster" + serialization-bindings { + "org.spongepowered.downloads.akka.AkkaSerializable" = jackson-json + } + } + +} diff --git a/artifacts/server/src/test/resources/application-test.yaml b/artifacts/server/src/test/resources/application-test.yaml new file mode 100644 index 00000000..7ab844b7 --- /dev/null +++ b/artifacts/server/src/test/resources/application-test.yaml @@ -0,0 +1,22 @@ +test-resources: + containers: + postgres: + image-name: postgres:14.6 + username: testuser + password: testpassword + db-name: testdb + +liquibase: + enabled: true + datasources: + default: + change-log: 'classpath:db/liquibase-changelog.xml' # (4) +endpoints: + liquibase: + enabled: true + sensitive: false +micronaut: + environment: test + http: + client: + read-timeout: 5m diff --git a/artifacts/src/main/resources/logback.xml b/artifacts/server/src/test/resources/logback.xml similarity index 84% rename from artifacts/src/main/resources/logback.xml rename to artifacts/server/src/test/resources/logback.xml index 6010eb52..d8294a83 100644 --- a/artifacts/src/main/resources/logback.xml +++ b/artifacts/server/src/test/resources/logback.xml @@ -9,7 +9,12 @@ - + + + + + + diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java deleted file mode 100644 index 4af14913..00000000 --- a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/Application.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.spongepowered.downloads.artifacts.server; - -import io.micronaut.context.event.ApplicationEventListener; -import io.micronaut.runtime.Micronaut; -import io.micronaut.runtime.server.event.ServerStartupEvent; -import io.swagger.v3.oas.annotations.*; -import io.swagger.v3.oas.annotations.info.*; - -@OpenAPIDefinition( - info = @Info( - title = "artifacts", - version = "0.0" - ) -) -public class Application implements ApplicationEventListener { - - public static void main(String[] args) { - Micronaut.run(Application.class, args); - } -} diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java deleted file mode 100644 index 960d4ab5..00000000 --- a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/ArtifactsQuery.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.query; - -import io.micronaut.http.annotation.Controller; - -@Controller("/groups/{groupID}/artifacts") -public class ArtifactsQuery { - - -} diff --git a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java b/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java deleted file mode 100644 index 6c5b218c..00000000 --- a/artifacts/src/main/java/org/spongepowered/downloads/artifacts/server/query/GroupsQueryController.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.spongepowered.downloads.artifacts.server.query; - -import akka.actor.typed.ActorSystem; -import akka.actor.typed.SpawnProtocol; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.Post; -import jakarta.inject.Inject; - -@Controller("/groups") -public class GroupsQueryController { - - - @Inject - private ActorSystem system; - @Inject - private ClusterSharding sharding; - - @Post("/") - public HttpResponse registerGroup( - @Body GroupRegistration.RegisterGroupRequest req - ) { - return null; - } -} diff --git a/artifacts/src/main/resources/application.toml b/artifacts/src/main/resources/application.toml deleted file mode 100644 index 250d1b57..00000000 --- a/artifacts/src/main/resources/application.toml +++ /dev/null @@ -1,10 +0,0 @@ -micronaut.application.name = 'artifacts' -netty.default.allocator.max-order = 3 - -[r2dbc.datasources.default] -schema-generate = 'CREATE_DROP' -dialect = 'H2' - -[micronaut.security] -authentication = 'bearer' -token.jwt.signatures.secret.generator.secret = '${JWT_GENERATOR_SIGNATURE_SECRET:pleaseChangeThisSecretForANewOne}' diff --git a/artifacts/src/main/resources/bootstrap.toml b/artifacts/src/main/resources/bootstrap.toml deleted file mode 100644 index 82bc8221..00000000 --- a/artifacts/src/main/resources/bootstrap.toml +++ /dev/null @@ -1,4 +0,0 @@ - -[kubernetes.client.discovery] -mode = 'endpoint' -mode-configuration.endpoint.watch.enabled = true diff --git a/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java b/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java deleted file mode 100644 index 13573ede..00000000 --- a/artifacts/src/test/java/org/spongepowered/downloads/artifacts/ArtifactsTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.spongepowered.downloads.artifacts; - -import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Assertions; - -import jakarta.inject.Inject; - -@MicronautTest(transactional = false) -class ArtifactsTest { - - @Inject - EmbeddedApplication application; - - @Test - void testItWorks() { - Assertions.assertTrue(application.isRunning()); - } - -} diff --git a/artifacts/worker/build.gradle.kts b/artifacts/worker/build.gradle.kts index 313f4efe..62090d55 100644 --- a/artifacts/worker/build.gradle.kts +++ b/artifacts/worker/build.gradle.kts @@ -1,47 +1,64 @@ +import org.gradle.kotlin.dsl.version +plugins { + id("com.github.johnrengelman.shadow") + id("io.micronaut.minimal.application") + id("io.micronaut.docker") + id("io.micronaut.test-resources") +} -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project +micronaut { + runtime("netty") + testRuntime("junit5") + processing { + incremental(true) + annotations("org.spongepowered.downloads.worker.*") + } + testResources { +// additionalModules.add("hibernate-reactive-postgresql") +// sharedServer.set(true) + } +} dependencies { implementation(project(":artifacts:api")) - annotationProcessor("io.micronaut.data:micronaut-data-processor") - annotationProcessor("io.micronaut:micronaut-http-validation") + implementation(project(":artifacts:events")) + implementation(libs.vavr) + + // Jackson + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") + annotationProcessor("io.micronaut.validation:micronaut-validation-processor") + implementation("io.micronaut:micronaut-jackson-databind") + implementation("io.micronaut.serde:micronaut-serde-jackson") + implementation("io.micronaut.validation:micronaut-validation") + + // validation + + implementation("jakarta.annotation:jakarta.annotation-api") + implementation(libs.jakarta.validation) + + annotationProcessor("io.micronaut.microstream:micronaut-microstream-annotations") annotationProcessor("io.micronaut.openapi:micronaut-openapi") annotationProcessor("io.micronaut.security:micronaut-security-annotations") - annotationProcessor("io.micronaut.serde:micronaut-serde-processor") - implementation("com.ongres.scram:client:2.1") - implementation("io.micronaut:micronaut-http-client") + // DB Access + annotationProcessor("io.micronaut.data:micronaut-data-processor") implementation("io.micronaut:micronaut-jackson-databind") implementation("io.micronaut.data:micronaut-data-r2dbc") + implementation("io.micronaut.liquibase:micronaut-liquibase") - implementation("io.micronaut.reactor:micronaut-reactor") - implementation("io.micronaut.reactor:micronaut-reactor-http-client") - implementation("io.micronaut.security:micronaut-security-ldap") - implementation("io.micronaut.serde:micronaut-serde-jackson") - implementation("io.micronaut.toml:micronaut-toml") - implementation("io.micronaut.xml:micronaut-jackson-xml") - implementation("io.swagger.core.v3:swagger-annotations") + implementation("io.micronaut.sql:micronaut-jdbc-hikari") + runtimeOnly("ch.qos.logback:logback-classic") + runtimeOnly("org.postgresql:postgresql") implementation("io.vertx:vertx-pg-client") - implementation("jakarta.annotation:jakarta.annotation-api") - implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) - implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") - implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") - implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") +// 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") - runtimeOnly("org.postgresql:postgresql") - runtimeOnly("org.postgresql:r2dbc-postgresql") - compileOnly("org.graalvm.nativeimage:svm") +// compileOnly("org.graalvm.nativeimage:svm") - implementation("io.micronaut:micronaut-validation") +// 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 new file mode 100644 index 00000000..50d8178d --- /dev/null +++ b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/ArtifactReadside.java @@ -0,0 +1,102 @@ +/* + * 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.worker.readside; + +import akka.persistence.query.typed.EventEnvelope; +import akka.projection.r2dbc.javadsl.R2dbcHandler; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; + + +@Singleton +public class ArtifactReadside extends R2dbcHandler> { + + @Inject + public ArtifactReadside(final ReadSide readSide) { + readSide.register(DetailsWriter.class); + } + + 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/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/JpaArtifact.java b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/JpaArtifact.java new file mode 100644 index 00000000..7d62dee2 --- /dev/null +++ b/artifacts/worker/src/main/java/org/spongepowered/downloads/artifacts/worker/readside/JpaArtifact.java @@ -0,0 +1,114 @@ +/* + * 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.worker.readside; + +import jakarta.persistence.Entity; +import java.util.Objects; +import io.micronaut.serde.annotation.Serdeable; + +@Entity(name = "Artifact") +@Table(name = "artifacts", + schema = "version") +@NamedQueries({ + @NamedQuery(name = "Artifact.findById", + query = "select a from Artifact a where a.groupId = :groupId and a.artifactId = :artifactId" + ) +}) +@MappedEntity +@Serderable +public class JpaArtifact { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", + updatable = false, + nullable = false) + private long id; + + @Column(name = "group_id", + nullable = false) + private String groupId; + + @Column(name = "artifact_id", + nullable = false) + private String artifactId; + + @Column(name = "display_name") + private String displayName; + + @Column(name = "website") + private String website; + + @Column(name = "git_repository") + private String gitRepo; + + @Column(name = "issues") + private String issues; + + public void setGroupId(final String groupId) { + this.groupId = groupId; + } + + public void setArtifactId(final String artifactId) { + this.artifactId = artifactId; + } + + public void setDisplayName(final String displayName) { + this.displayName = displayName; + } + + public void setWebsite(final String website) { + this.website = website; + } + + public void setGitRepo(final String gitRepo) { + this.gitRepo = gitRepo; + } + + public void setIssues(final String issues) { + this.issues = issues; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JpaArtifact that = (JpaArtifact) o; + return id == that.id && Objects.equals(groupId, that.groupId) && Objects.equals( + artifactId, that.artifactId) && Objects.equals( + displayName, that.displayName) && Objects.equals( + website, that.website) && Objects.equals(gitRepo, that.gitRepo) && Objects.equals( + issues, that.issues); + } + + @Override + public int hashCode() { + return Objects.hash(id, groupId, artifactId, displayName, website, gitRepo, issues); + } +} diff --git a/artifacts/worker/src/main/resources/application.conf b/artifacts/worker/src/main/resources/application.conf new file mode 100644 index 00000000..997ab44f --- /dev/null +++ b/artifacts/worker/src/main/resources/application.conf @@ -0,0 +1,32 @@ +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 { + journal.payload-column-type = JSONB + snapshot.payload-column-type = JSONB + state.payload-column-type = JSONB +} +akka.serialization.jackson.jackson-json.compression.algorithm = off + + +akka.persistence.r2dbc { + dialect = "postgres" + connection-factory { + driver = "postgres" + 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/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthService.java b/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthService.java deleted file mode 100644 index f7ae9117..00000000 --- a/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthService.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.auth.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.transport.Method; - -public interface AuthService extends Service { - - final class Providers { - - public static final String LDAP = "ldap"; - } - - // The response will contain a JWT if the authentication succeeded. - // Uses standard Basic auth over HTTPS to login. - ServiceCall login(); - - ServiceCall logout(); - - default Descriptor descriptor() { - return Service.named("auth") - .withCalls( - Service.restCall(Method.POST, "/auth/login", this::login), - Service.restCall(Method.POST, "/auth/logout", this::logout) - ) - .withAutoAcl(true); - } -} diff --git a/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthenticationRequest.java b/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthenticationRequest.java deleted file mode 100644 index 14bdde59..00000000 --- a/auth-api/src/main/java/org/spongepowered/downloads/auth/api/AuthenticationRequest.java +++ /dev/null @@ -1,36 +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.auth.api; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -public final class AuthenticationRequest { - - @JsonSerialize - public final record Response(@JsonProperty("token") String jwtToken) { - } - -} diff --git a/auth-api/src/main/java/org/spongepowered/downloads/auth/api/utils/AuthUtils.java b/auth-api/src/main/java/org/spongepowered/downloads/auth/api/utils/AuthUtils.java deleted file mode 100644 index 84f348f1..00000000 --- a/auth-api/src/main/java/org/spongepowered/downloads/auth/api/utils/AuthUtils.java +++ /dev/null @@ -1,144 +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.auth.api.utils; - -import com.lightbend.lagom.javadsl.api.ServiceCall; -import io.vavr.collection.List; -import org.pac4j.core.authorization.authorizer.Authorizer; -import org.pac4j.core.client.Client; -import org.pac4j.core.client.DirectClient; -import org.pac4j.core.config.Config; -import org.pac4j.core.context.HttpConstants; -import org.pac4j.core.credentials.TokenCredentials; -import org.pac4j.core.profile.CommonProfile; -import org.pac4j.http.client.direct.HeaderClient; -import org.pac4j.jwt.config.encryption.EncryptionConfiguration; -import org.pac4j.jwt.config.encryption.SecretEncryptionConfiguration; -import org.pac4j.jwt.config.signature.SecretSignatureConfiguration; -import org.pac4j.jwt.config.signature.SignatureConfiguration; -import org.pac4j.jwt.credentials.authenticator.JwtAuthenticator; -import org.pac4j.jwt.profile.JwtGenerator; - -import javax.inject.Inject; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Date; - -public final record AuthUtils(String encryptionSecret, String signatureSecret, - String nexusWebhookSecret, String internalHeaderSecret, - String internalHeaderKey) { - - @SuppressWarnings("rawtypes") - public Config config(final Client... additionalClients) { - final var jwtClient = this.createJwtClient(); - final var config = new Config(List.of(jwtClient).appendAll(List.of(additionalClients)).asJava()); - config.getClients().setDefaultSecurityClients(jwtClient.getName()); - this.setAuthorizers(config); - return config; - } - - public ServiceCall internalAuth(ServiceCall call) { - return call.handleRequestHeader(header -> header.withHeader(this.internalHeaderKey, this.internalHeaderSecret)); - } - - - private DirectClient createJwtClient() { - final var headerClient = new HeaderClient(); - headerClient.setName(AuthUtils.Types.JWT); - headerClient.setHeaderName(HttpConstants.AUTHORIZATION_HEADER); - headerClient.setPrefixHeader(HttpConstants.BEARER_HEADER_PREFIX); - - final var jwtAuthenticator = new JwtAuthenticator(); - if (!this.signatureSecret.isBlank()) { - jwtAuthenticator.addSignatureConfiguration(this.getSignatureConfiguration()); - } - if (!this.encryptionSecret.isBlank()) { - jwtAuthenticator.addEncryptionConfiguration(this.getEncryptionConfiguration()); - } - headerClient.setAuthenticator(jwtAuthenticator); // this should provide the correct profile automagically. - headerClient.setName(AuthUtils.Types.JWT); - return headerClient; - } - - public JwtGenerator createJwtGenerator() { - final var generator = new JwtGenerator<>(); - if (!this.signatureSecret.isBlank()) { - generator.setSignatureConfiguration(this.getSignatureConfiguration()); - } - if (!this.encryptionSecret.isBlank()) { - generator.setEncryptionConfiguration(this.getEncryptionConfiguration()); - } - generator.setExpirationTime(Date.from(Instant.now().plus(10, ChronoUnit.MINUTES))); - return generator; - } - - private void setAuthorizers(final Config config) { - config.addAuthorizer(Roles.ADMIN, Authorizers.ADMIN); - config.addAuthorizer(Roles.WEBHOOK, Authorizers.WEBHOOK); - } - - private EncryptionConfiguration getEncryptionConfiguration() { - return new SecretEncryptionConfiguration(this.encryptionSecret); - } - - private SignatureConfiguration getSignatureConfiguration() { - return new SecretSignatureConfiguration(this.signatureSecret); - } - - private SignatureConfiguration getSonatypeSignatureConfiguration() { - return new SecretSignatureConfiguration(this.nexusWebhookSecret); - } - - @Inject - public static AuthUtils configure(com.typesafe.config.Config config) { - final var authConfig = config.getConfig("systemofadownload.auth.secrets"); - final var encryptionSecret = authConfig.getString("encryption"); - final var signatureSecret = authConfig.getString("signature"); - final var nexusWebhookSecret = authConfig.getString("nexus-webhook"); - final var internalHeaderKey = authConfig.getString("internal-header"); - final var internalHeaderSecret = authConfig.getString("internal-secret"); - return new AuthUtils( - encryptionSecret, signatureSecret, nexusWebhookSecret, internalHeaderSecret, internalHeaderKey); - } - - static final class Authorizers { - static final Authorizer ADMIN = - (webContext, list) -> list.stream().anyMatch( - x -> !x.isExpired() && x.getRoles().contains(AuthUtils.Roles.ADMIN)); - static final Authorizer WEBHOOK = - (webContext, list) -> list.stream().anyMatch( - x -> !x.isExpired() && x.getRoles().contains(AuthUtils.Roles.WEBHOOK)); - } - - public static final class Types { - public static final String JWT = "jwt"; - public static final String WEBHOOK = "internal"; - } - - public static final class Roles { - public static final String ADMIN = "soad_admin"; - public static final String WEBHOOK = "soad_webhook"; - } -} diff --git a/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthModule.java b/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthModule.java deleted file mode 100644 index 2904d6c6..00000000 --- a/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthModule.java +++ /dev/null @@ -1,197 +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.auth; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.google.inject.name.Named; -import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; -import org.ldaptive.ConnectionConfig; -import org.ldaptive.DefaultConnectionFactory; -import org.ldaptive.auth.FormatDnResolver; -import org.ldaptive.auth.PooledBindAuthenticationHandler; -import org.ldaptive.pool.BlockingConnectionPool; -import org.ldaptive.pool.IdlePruneStrategy; -import org.ldaptive.pool.PoolConfig; -import org.ldaptive.pool.PooledConnectionFactory; -import org.ldaptive.pool.SearchValidator; -import org.pac4j.core.client.DirectClient; -import org.pac4j.core.config.Config; -import org.pac4j.core.credentials.UsernamePasswordCredentials; -import org.pac4j.core.credentials.authenticator.Authenticator; -import org.pac4j.core.exception.CredentialsException; -import org.pac4j.core.profile.CommonProfile; -import org.pac4j.http.client.direct.DirectBasicAuthClient; -import org.pac4j.jwt.profile.JwtGenerator; -import org.pac4j.ldap.profile.service.LdapProfileService; -import org.spongepowered.downloads.auth.api.AuthService; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import play.Environment; - -import javax.inject.Inject; -import java.time.Duration; -import java.util.Collection; -import java.util.concurrent.TimeUnit; - -// See: https://github.com/pac4j/lagom-pac4j-java-demo -// -// We could alternatively go for Cookie auth that uses LDAP as an initial check, but -// JWT may suffice - we can then just use LDAP as a way enable JWT generation. We can also just -// require LDAP every time... but that's probably not what we want. -// -// JWT is great as we won't want to store sessions. However, it's not great in the sense that it'll -// be hard to expire them when done with - but we could just generate secrets on the fly when necessary -// which expire themselves after some time, and can be forcefully expired when needed. -// -// JWTs should be short lived anyway. -public final class AuthModule extends AbstractModule implements ServiceGuiceSupport { - - private boolean useDummyCredentials; - private String ldapUrl; - private String ldapBaseUserDn; - private String ldapSoadOu; - - private Duration connectionTimeout; - private Duration responseTimeout; - private Duration blockWaitTime; - - private final com.typesafe.config.Config config; - private AuthUtils auth; - - @Inject - public AuthModule(final Environment environment, final com.typesafe.config.Config config) { - this.config = config; - } - - @Override - protected void configure() { - final var authConfig = this.config.getConfig("systemofadownload.auth"); - final var ldap = authConfig.getConfig("ldap"); - this.useDummyCredentials = authConfig.getBoolean("use-dummy-ldap"); - this.ldapUrl = ldap.getString("url"); - this.ldapBaseUserDn = ldap.getString("base-user-on"); - this.ldapSoadOu = ldap.getString("soad-ou"); - this.connectionTimeout = Duration.ofMillis(ldap.getDuration("connection-timeout", TimeUnit.MILLISECONDS)); - this.responseTimeout = Duration.ofSeconds(ldap.getDuration("response-timeout", TimeUnit.SECONDS)); - this.blockWaitTime = Duration.ofSeconds(ldap.getDuration("wait-time", TimeUnit.SECONDS)); - this.auth = AuthUtils.configure(this.config); - this.bindService(AuthService.class, AuthServiceImpl.class); - } - - // TODO: If we'd rather use a GraphQL endpoint to log in, rather than - // a header, then we don't need this - we do our LDAP query manually and - // create a JWT in the body of the request/response method. - @Provides - @Named(AuthService.Providers.LDAP) - protected DirectClient providerHeaderLDAPClient() { - final var basicAuthClient = new DirectBasicAuthClient(); - basicAuthClient.setName(AuthService.Providers.LDAP); - - final Authenticator authenticator; - // TODO: Ditch this once we're running. - if (this.useDummyCredentials) { - authenticator = ((usernamePasswordCredentials, webContext) -> { - if (usernamePasswordCredentials.getUsername().equals("soad") && usernamePasswordCredentials.getPassword().equals("systemofadownload")) { - final var profile = new CommonProfile(); - profile.setId("soad"); - usernamePasswordCredentials.setUserProfile(profile); - profile.addRole(AuthUtils.Roles.ADMIN); - return; - } - throw new CredentialsException("Incorrect username and password."); - }); - } else { - // http://www.pac4j.org/3.4.x/docs/authenticators/ldap.html - // This could probably be improved... but the documentation leaves something to be desired - final var dnResolver = new FormatDnResolver(); - dnResolver.setFormat("cn=%s," + this.ldapBaseUserDn); - final var connectionConfig = new ConnectionConfig(); - connectionConfig.setConnectTimeout(this.connectionTimeout); - connectionConfig.setResponseTimeout(this.responseTimeout); - connectionConfig.setLdapUrl(this.ldapUrl); - final var connectionFactory = new DefaultConnectionFactory(); - connectionFactory.setConnectionConfig(connectionConfig); - final var poolConfig = new PoolConfig(); - poolConfig.setMinPoolSize(1); - poolConfig.setMaxPoolSize(2); - poolConfig.setValidateOnCheckOut(true); - poolConfig.setValidateOnCheckIn(true); - poolConfig.setValidatePeriodically(false); - final var searchValidator = new SearchValidator(); - final var pruneStrategy = new IdlePruneStrategy(); - final var connectionPool = new BlockingConnectionPool(); - connectionPool.setPoolConfig(poolConfig); - connectionPool.setBlockWaitTime(this.blockWaitTime); - connectionPool.setValidator(searchValidator); - connectionPool.setPruneStrategy(pruneStrategy); - connectionPool.setConnectionFactory(connectionFactory); - connectionPool.initialize(); - final var pooledConnectionFactory = new PooledConnectionFactory(); - pooledConnectionFactory.setConnectionPool(connectionPool); - // dnResolver.setConnectionFactory(pooledConnectionFactory); - final var handler = new PooledBindAuthenticationHandler(); - handler.setConnectionFactory(pooledConnectionFactory); - final var ldaptiveAuthenticator = new org.ldaptive.auth.Authenticator(); - ldaptiveAuthenticator.setDnResolver(dnResolver); - ldaptiveAuthenticator.setAuthenticationHandler(handler); - - authenticator = new LdapProfileService(pooledConnectionFactory, ldaptiveAuthenticator, "ou", this.ldapBaseUserDn); - } - - // If we're IP whitelisting it, we just need to check the webcontext. Otherwise we'll want to - // add tokens and such - basicAuthClient.setAuthenticator(authenticator); - basicAuthClient.setAuthorizationGenerator((webContext, profile) -> { - final var ouAttr = profile.getAttribute("ou"); - final boolean isAdmin; - if (ouAttr instanceof String) { - isAdmin = ouAttr.equals(this.ldapSoadOu); - } else if (ouAttr instanceof Collection) { - isAdmin = ((Collection) ouAttr).contains(this.ldapSoadOu); - } else { - isAdmin = false; - } - - if (isAdmin) { - profile.addRole(AuthUtils.Roles.ADMIN); - } - return profile; - }); - return basicAuthClient; - } - - @Provides - @SOADAuth - protected JwtGenerator provideJwtGenerator() { - return this.auth.createJwtGenerator(); - } - - @Provides - @SOADAuth - protected Config configProvider(@Named(AuthService.Providers.LDAP) final DirectClient ldapClient) { - return this.auth.config(ldapClient); - } - -} diff --git a/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthServiceImpl.java b/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthServiceImpl.java deleted file mode 100644 index 632a3242..00000000 --- a/auth-impl/src/main/java/org/spongepowered/downloads/auth/AuthServiceImpl.java +++ /dev/null @@ -1,81 +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.auth; - -import akka.NotUsed; -import com.google.inject.Inject; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import org.pac4j.core.config.Config; -import org.pac4j.core.profile.CommonProfile; -import org.pac4j.jwt.profile.JwtGenerator; -import org.pac4j.lagom.javadsl.SecuredService; -import org.spongepowered.downloads.auth.api.AuthService; -import org.spongepowered.downloads.auth.api.AuthenticationRequest; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; - -import java.sql.Date; -import java.time.Duration; -import java.time.Instant; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; - -public final class AuthServiceImpl implements AuthService, SecuredService { - - private final Config securityConfig; - private final JwtGenerator profileJwtGenerator; - private final Duration expirationTime; - - @Inject - public AuthServiceImpl( - @SOADAuth final Config config, @SOADAuth final JwtGenerator profileJwtGenerator, - com.typesafe.config.Config applicationConfig - ) { - this.securityConfig = config; - this.profileJwtGenerator = profileJwtGenerator; - this.expirationTime = Duration.ofSeconds( - applicationConfig.getDuration("systemofadownload.auth.expiration", TimeUnit.SECONDS)); - } - - @Override - public ServiceCall login() { - return this.authorize(Providers.LDAP, AuthUtils.Roles.ADMIN, profile -> { - this.profileJwtGenerator.setExpirationTime(Date.from(Instant.now().plus(this.expirationTime))); - return notUsed -> CompletableFuture.completedFuture(new AuthenticationRequest.Response(this.profileJwtGenerator.generate(profile))); - }); - } - - @Override - public ServiceCall logout() { - // TODO - if it's even possible - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> { - return notUsed -> CompletableFuture.completedFuture(NotUsed.getInstance()); - }); - } - - @Override - public Config getSecurityConfig() { - return this.securityConfig; - } -} diff --git a/auth-impl/src/main/resources/application.conf b/auth-impl/src/main/resources/application.conf deleted file mode 100644 index 31b86ef2..00000000 --- a/auth-impl/src/main/resources/application.conf +++ /dev/null @@ -1,20 +0,0 @@ - -play.http.secret.key = ${?APPLICATION_SECRET} - -play.modules { - enabled += "org.spongepowered.downloads.auth.AuthModule" -} - -systemofadownload.auth { - use-dummy-ldap = true - expiration = "1h" -} - -play.filters.enabled += "play.filters.cors.CORSFilter" -play.filters.csrf.header.protectHeaders = null -play.filters.disabled += "play.filters.csrf.CSRFFilter" -play.filters.cors { - pathPrefixes = ["/auth"] - allowedHttpMethods = ["GET", "POST"] - preflightMaxAge = 3 days -} diff --git a/auth-impl/src/main/resources/logback.xml b/auth-impl/src/main/resources/logback.xml deleted file mode 100644 index 9bd2dad8..00000000 --- a/auth-impl/src/main/resources/logback.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - System.out - - %date{hh:MM:ss.SSS} [%level] [%thread] [%logger{5}/%marker] - %coloredLevel %msg%n - - - - - 8192 - true - - - - - - - - - - - - - - - - diff --git a/auth-impl/src/main/resources/reference.conf b/auth-impl/src/main/resources/reference.conf deleted file mode 100644 index 9f66f9c6..00000000 --- a/auth-impl/src/main/resources/reference.conf +++ /dev/null @@ -1,13 +0,0 @@ -systemofadownload.auth { - use-dummy-ldap = false - expiration = "300s" - ldap { - url = "ldap://localhost:389" - base-user-on = "dc=spongepowered,dc=org" - soad-ou = "soad" - - connection-timeout = 500 - response-timeout = "1s" - wait-time = "1s" - } -} diff --git a/build.gradle.kts b/build.gradle.kts index 026df055..aaf4fe26 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,117 +1,65 @@ - +import org.gradle.plugins.ide.idea.model.IdeaLanguageLevel version = "0.1" group = "systemofadownload" +plugins { + `java-library` + id("com.github.johnrengelman.shadow") version "8.1.1" apply false + id("io.micronaut.library") version "4.0.2" apply false + id("io.micronaut.application") version "4.0.2" apply false + id("io.micronaut.docker") version "4.0.2" apply false + id("io.micronaut.test-resources") version "4.0.2" apply false +} + repositories { mavenCentral() } -plugins { - `java-library` - `application` - id("com.github.johnrengelman.shadow") - id("io.micronaut.application") - id("io.micronaut.test-resources") +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") + } } -val akkaVersion: String by project -val scalaVersion: String by project -val akkaManagementVersion: String by project -val akkaProjection: String by project allprojects { - apply(plugin = "java-library") - apply(plugin = "io.micronaut.application") - apply(plugin = "io.micronaut.test-resources") - apply(plugin = "com.github.johnrengelman.shadow") - java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - if (JavaVersion.current() < JavaVersion.VERSION_17) { + sourceCompatibility = JavaVersion.VERSION_20 + targetCompatibility = JavaVersion.VERSION_20 + if (JavaVersion.current() < JavaVersion.VERSION_20) { toolchain { - languageVersion.set(JavaLanguageVersion.of(17)) + languageVersion.set(JavaLanguageVersion.of(20)) } } } -} - -dependencies { - annotationProcessor("io.micronaut.data:micronaut-data-processor") - annotationProcessor("io.micronaut:micronaut-http-validation") - annotationProcessor("io.micronaut.openapi:micronaut-openapi") - annotationProcessor("io.micronaut.security:micronaut-security-annotations") - annotationProcessor("io.micronaut.serde:micronaut-serde-processor") - implementation("com.ongres.scram:client:2.1") - implementation("io.micronaut:micronaut-http-client") - implementation("io.micronaut:micronaut-jackson-databind") - implementation("io.micronaut.data:micronaut-data-r2dbc") - implementation("io.micronaut.liquibase:micronaut-liquibase") - implementation("io.micronaut.reactor:micronaut-reactor") - implementation("io.micronaut.reactor:micronaut-reactor-http-client") - implementation("io.micronaut.security:micronaut-security-ldap") - implementation("io.micronaut.serde:micronaut-serde-jackson") - implementation("io.micronaut.toml:micronaut-toml") - implementation("io.micronaut.xml:micronaut-jackson-xml") - implementation("io.swagger.core.v3:swagger-annotations") - implementation("io.vertx:vertx-pg-client") - implementation("jakarta.annotation:jakarta.annotation-api") - implementation(platform("com.typesafe.akka:akka-bom_${scalaVersion}:${akkaVersion}")) - implementation("com.typesafe.akka:akka-actor-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-persistence-typed_${scalaVersion}") - implementation("com.lightbend.akka:akka-projection-core_${scalaVersion}:${akkaProjection}") - implementation("com.typesafe.akka:akka-cluster-sharding-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-cluster-typed_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.typesafe.akka:akka-discovery_${scalaVersion}") - implementation("com.lightbend.akka.management:akka-management_${scalaVersion}:${akkaManagementVersion}") - implementation("com.lightbend.akka.management:akka-management-cluster-bootstrap_${scalaVersion}:${akkaManagementVersion}") - - runtimeOnly("ch.qos.logback:logback-classic") - runtimeOnly("org.postgresql:postgresql") - runtimeOnly("org.postgresql:r2dbc-postgresql") - compileOnly("org.graalvm.nativeimage:svm") - - implementation("io.micronaut:micronaut-validation") - -} - - -application { - mainClass.set("systemofadownload.Application") -} -tasks { - dockerBuild { - images.add("${project.name}:${project.version}") - } - dockerBuildNative { - images.add("${project.name}:${project.version}") - - } -} -graalvmNative.toolchainDetection.set(false) -micronaut { - runtime("netty") - testRuntime("junit5") - processing { - incremental(true) - annotations("systemofadownload.*") - } - testResources { - additionalModules.add("r2dbc-postgresql") - } -} -graalvmNative { - binaries { - named("main") { - imageName.set("mn-graalvm-application") - buildArgs("--verboase") + tasks { + withType { + options.compilerArgs.add("--enable-preview") + } + withType { + jvmArgs("--enable-preview") } } + } + diff --git a/build.sbt b/build.sbt deleted file mode 100644 index 1cd077bb..00000000 --- a/build.sbt +++ /dev/null @@ -1,358 +0,0 @@ -import com.lightbend.lagom.core.LagomVersion -import com.typesafe.sbt.packager.docker.DockerChmodType -import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport.{HeaderLicenseStyle, headerLicenseStyle} - -import scala.sys.process.Process - -ThisBuild / organization := "org.spongepowered" -ThisBuild / version := "0.2-SNAPSHOT" -ThisBuild / scalaVersion := "2.13.8" - -// License setup -ThisBuild / organizationName := "SpongePowered" -ThisBuild / startYear := Some(2020) -ThisBuild / licenses += ("MIT", url("https://opensource.org/licenses/MIT")) -ThisBuild / scmInfo := Some(ScmInfo(url("https://github.com/SpongePowered/SystemOfADownload"), - "scm:git@github.com:spongepowered/systemofadownload.git")) -ThisBuild / developers := List( - Developer( - id = "gabizou", - name = "Gabriel Harris-Rouquette", - email = "gabizou@spongepowered.org", - url = url("https://github.com/gabizou") - ) -) -ThisBuild / description := "A Web Application for indexing and cataloging Artifacts in Maven Repositories" -ThisBuild / homepage := Some(url("https://github.com/SpongePowered/SystemOfADownload")) -ThisBuild / pomIncludeRepository := { _ => false } -ThisBuild / publishMavenStyle := true -ThisBuild / versionScheme := Some("early-semver") - -// Basically, because of sbt's limited publishing to only one repository, -// we can take advantage of environment variables to publish to multiple -// repositories within the same job. -ThisBuild / publishTo := { - (sys.env.get("REPO_NAME"), sys.env.get("REPO_CREDENTIAL_FILE"), sys.env.get("SONATYPE_SNAPSHOT_REPO"), sys.env.get("SONATYPE_RELEASE_REPO")) match { - case (Some(name), Some(repoTarget), Some(snapshotRepo), Some(releaseRepo)) => { - credentials += Credentials(Path.userHome / ".sbt" / repoTarget) - if (isSnapshot.value) - Some(name at snapshotRepo) - else - Some(name at releaseRepo) - } - case (_, _, _, _) => None - } -} - -// Deployed Repositories -// TODO - Figure out deploying to our sonatype and to maven central -// Then also figure out deploying docker images??? - -// Liquibase Docker Tasks -lazy val buildLiquibaseImage = taskKey[Unit]("Build the Liquibase docker image") -val rootFilter = ScopeFilter(inProjects(soadRoot), inConfigurations(Compile)) -buildLiquibaseImage := { - val versionTag = version.all(rootFilter).value.head - Process(Seq("docker", "buildx", "build", "-t", s"spongepowered/systemofadownload-liquibase:$versionTag", "./liquibase/", "-f", "./liquibase/Dockerfile")).! -} - -lazy val runLiquibase = taskKey[Unit]("Runs the liquibase migration against a local dev database") - -lazy val setupDevEnvironment = taskKey[Unit]("Runs the necessary commands to set up a local environment to run the application") - -lazy val setupPostgres = taskKey[Unit]("Runs a postgres instance for local development") -lazy val setupKafka = taskKey[Unit]("Runs Kafka and zookeeper instance for local development") -setupDevEnvironment := { - setupPostgres.value - runLiquibase.value - setupKafka.value -} -setupPostgres := { - Process(Seq("sh", - s"${soadRoot.base.absolutePath}/dev/run_postgres.sh", - )).! -} - -setupKafka := { - Process(Seq("sh", s"${soadRoot.base.absolutePath}/dev/run_kafka.sh")).! -} - -runLiquibase := { - Process(Seq("docker", - "run", - "--rm", - "--mount", s"type=bind,source=${soadRoot.base.absolutePath}/liquibase/changelog,target=/liquibase/changelog,readonly", - "--network=host", - "liquibase/liquibase", - "--logLevel=info", - s"--url=jdbc:postgresql://localhost:5432/default", - "--defaultsFile=/liquibase/changelog/liquibase.properties", - "--changeLogFile=changelog.xml", - "--classpath=/liquibase/changelog", - "--username=admin", - "--password=password", - "update")).! -} - -// region dependency versions - -//noinspection SbtDependencyVersionInspection -ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-java8-compat" % "always" - -lazy val vavr = "io.vavr" % "vavr" % "0.10.4" -lazy val vavrJackson = "io.vavr" % "vavr-jackson" % "0.10.3" - -lazy val pac4jHttp = "org.pac4j" % "pac4j-http" % "3.7.0" -lazy val pac4jJwt = "org.pac4j" % "pac4j-jwt" % "3.7.0" - -lazy val lagomPac4j = "org.pac4j" %% "lagom-pac4j" % "2.2.1" -lazy val lagomPac4jLdap = "org.pac4j" % "pac4j-ldap" % "3.7.0" - -// Enable Junit5 -lazy val junit = "org.junit.jupiter" % "junit-jupiter-api" % "5.9.0" % Test -// sbt-jupiter-interface -lazy val jupiterInterface = "net.aichler" % "jupiter-interface" % "0.11.1" % Test - - -// Play jackson uses 2.11, but 2.12 is backwards compatible -lazy val jacksonDataBind = "com.fasterxml.jackson.core" % "jackson-databind" % "2.14.0" -lazy val jacksonDataTypeJsr310 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.14.0" -lazy val jacksonDataformatXml = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.14.0" -lazy val jacksonDataformatCbor = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % "2.14.0" -lazy val jacksonDatatypeJdk8 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.14.0" -lazy val jacksonParameterNames = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.14.0" -lazy val jacksonParanamer = "com.fasterxml.jackson.module" % "jackson-module-paranamer" % "2.14.0" -lazy val jacksonScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.14.0" -lazy val jacksonGuava = "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % "2.14.0" -lazy val jacksonPcollections = "com.fasterxml.jackson.datatype" % "jackson-datatype-pcollections" % "2.14.0" -// endregion - -lazy val akkaHttp = "com.typesafe.akka" %% "akka-http" % LagomVersion.akkaHttp -lazy val akkaJackson = "com.typesafe.akka" %% "akka-http-jackson" % LagomVersion.akkaHttp - -lazy val akkaStreamTyped = "com.typesafe.akka" %% "akka-stream-typed" % LagomVersion.akka -lazy val akkaPersistenceTestkit = "com.typesafe.akka" %% "akka-persistence-testkit" % LagomVersion.akka % Test -lazy val akkaKubernetesDiscovery = "com.lightbend.akka.discovery" %% "akka-discovery-kubernetes-api" % "1.1.3" - -lazy val playFilterHelpers = "com.typesafe.play" %% "filters-helpers" % LagomVersion.play - -lazy val hibernate = "org.hibernate" % "hibernate-core" % "5.5.6" -lazy val postgres = "org.postgresql" % "postgresql" % "42.5.0" -lazy val hibernateTypes = "com.vladmihalcea" % "hibernate-types-55" % "2.20.0" - -lazy val guice = "com.google.inject" % "guice" % "5.1.0" - -lazy val jgit = "org.eclipse.jgit" % "org.eclipse.jgit" % "6.1.0.202203080745-r" -lazy val jgit_jsch = "org.eclipse.jgit" % "org.eclipse.jgit.ssh.jsch" % "6.1.0.202203080745-r" - -lazy val mavenArtifact = "org.apache.maven" % "maven-artifact" % "3.8.5" - -lazy val testContainers = "org.testcontainers" % "testcontainers" % "1.17.3" % Test -lazy val testContainersJunit = "org.testcontainers" % "junit-jupiter" % "1.17.3" % Test -lazy val testContainersPostgres = "org.testcontainers" % "postgresql" % "1.17.3" % Test - -// endregion - -// region - project blueprints - -def soadProject(name: String) = - Project(name, file(name)).settings( - moduleName := s"systemofadownload-$name", - Compile / javacOptions := Seq("--release", "17", "--enable-preview", "-parameters", "-encoding", "UTF-8"), //Override the settings Lagom sets - artifactName := { (_: ScalaVersion, module: ModuleID, artifact: Artifact) => - s"${artifact.name}-${module.revision}.${artifact.extension}" - }, - Compile / doc / sources := Seq.empty, - Compile / packageDoc / publishArtifact := false, - compileOrder := CompileOrder.JavaThenScala, //Needed so scalac doesn't try to parse the files - headerLicense := Some(HeaderLicense.Custom( - """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. - |""".stripMargin - )), - headerLicenseStyle := HeaderLicenseStyle.SpdxSyntax, - headerEmptyLine := false - - ) - -def apiSoadProject(name: String) = - soadProject(name).settings( - libraryDependencies ++= Seq( - // Lagom Java API - lagomJavadslApi, - // Bump Jackson over Lagom's jackson - jacksonDataBind, - jacksonDataTypeJsr310, - jacksonDataformatCbor, - jacksonDatatypeJdk8, - jacksonParameterNames, - jacksonParanamer, - jacksonScala, - jacksonGuava, // Eventually not needed when we migrate off lagom - jacksonPcollections, // Eventually not needed when we migrate off lagom - // Then add the Lagom Jackson modules - lagomJavadslJackson, - //Language Features - vavr, - // Override guice from Lagom to support Java 16 - guice - ) - ) - -def serverSoadProject(name: String) = - soadProject(name) - .enablePlugins(LagomJava, DockerPlugin) - .settings( - libraryDependencies ++= Seq( - // Bump Jackson over Lagom's jackson - jacksonDataBind, - jacksonDataTypeJsr310, - jacksonDataformatCbor, - jacksonDatatypeJdk8, - jacksonParameterNames, - jacksonParanamer, - jacksonScala, - jacksonGuava, // Eventually not needed when we migrate off lagom - jacksonPcollections, // Eventually not needed when we migrate off lagom - //Lagom Dependencies - // Specifically set up Akka-Clustering - lagomJavadslCluster, - // Set up Discovery between Services - lagomJavadslAkkaDiscovery, - akkaKubernetesDiscovery, - // I mean, we are a server, aren't we? - lagomJavadslServer, - // Set up logging - lagomLogback, - //Language Features for Serialization/Deserialization - vavrJackson, - // Override guice from Lagom to support Java 16 - guice, - // Ensure the play filter helpers are included - playFilterHelpers, - //Test Dependencies - lagomJavadslTestKit, - junit, // Always enable Junit 5 - jupiterInterface - ) - ).settings( - dockerUpdateLatest := true, - dockerBaseImage := "eclipse-temurin:17.0.3_7-jre", - dockerChmodType := DockerChmodType.UserGroupWriteExecute, - dockerExposedPorts := Seq(9000, 8558, 2552), - dockerLabels ++= Map( - "author" -> "spongepowered" - ), - Docker / maintainer := "spongepowered", - Docker / packageName := s"systemofadownload-$name", - dockerUsername := Some("spongepowered"), - Universal / javaOptions ++= Seq( - "-Dpidfile.path=/dev/null" - ) - ) - -def implSoadProject(name: String, implFor: Project) = - serverSoadProject(name).dependsOn( - //The service we're implementing - implFor - ).settings( - libraryDependencies ++= Seq( - // We use kafka for all inter-service message forwarding - lagomJavadslKafkaBroker - ) - ) - -def implSoadProjectWithPersistence(name: String, implFor: Project) = - implSoadProject(name, implFor).settings( - libraryDependencies ++= Seq( - //Lagom Database Dependencies - lagomJavadslPersistenceJpa, - //Database Dependencies - hibernate, - postgres, - akkaPersistenceTestkit - ) - ) - -// endregion - -// region Project Definitions - -lazy val `downloads-api` = soadProject("downloads-api").enablePlugins(DockerPlugin) - .settings( - libraryDependencies ++= Seq( - // App - mavenArtifact, - - // Akka - akkaHttp, - akkaStreamTyped, - akkaKubernetesDiscovery, - - // Jackson serialization - akkaJackson, - jacksonDataBind, - jacksonDataTypeJsr310, - jacksonDataformatCbor, - jacksonDatatypeJdk8, - jacksonParameterNames, - jacksonParanamer, - jacksonScala, - //Language Features - vavr, - // Persistence - hibernate, - postgres, - // Testing - akkaPersistenceTestkit, - junit, - jupiterInterface - ), - dockerUpdateLatest := true, - dockerBaseImage := "eclipse-temurin:17.0.3_7-jre", - dockerChmodType := DockerChmodType.UserGroupWriteExecute, - dockerExposedPorts := Seq(9000, 8558, 2552), - // dockerLabels ++= Map( - // "author" -> "spongepowered" - // ), - Docker / maintainer := "spongepowered", - Docker / packageName := s"systemofadownload-$name", - dockerUsername := Some("spongepowered"), - Universal / javaOptions ++= Seq( - "-Dpidfile.path=/dev/null" - ) - ) - - -// endregion - -lazy val soadRoot = project.in(file(".")).settings( - name := "SystemOfADownload" -).aggregate( -) - -ThisBuild / lagomCassandraEnabled := false -ThisBuild / lagomKafkaEnabled := false -ThisBuild / lagomKafkaPort := 9092 -ThisBuild / lagomServicesPortRange := PortRange(21000, 23000) - diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..dee3e800 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,9 @@ + +dependencyResolutionManagement { + + versionCatalogs { + create("akka") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts deleted file mode 100644 index 58082c01..00000000 --- a/buildSrc/src/main/kotlin/soad.java-conventions.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id("java") - id("application") - id("com.github.johnrengelman.shadow") version "7.1.2" - id("io.micronaut.application") version "3.7.0" - id("io.micronaut.test-resources") version "3.7.0" -} - -group = "org.spongepowered" -version = "1.0" -repositories { - mavenCentral() -} - -dependencies { - - id("com.github.johnrengelman.shadow") version "7.1.2" - id("io.micronaut.application") version "3.7.0" - id("io.micronaut.test-resources") version "3.7.0" -} diff --git a/dev/run_postgres.sh b/dev/run_postgres.sh index 3cdf3bf6..c4760d07 100755 --- a/dev/run_postgres.sh +++ b/dev/run_postgres.sh @@ -17,6 +17,6 @@ if [ ! "$(docker ps -q -f name="${postgres_name}")" ]; then -e POSTGRES_PASSWORD=password \ -e POSTGRES_DB=default \ -p 5432:5432 \ - postgres:13-alpine \ + postgres:15-alpine \ postgres -N 500 fi diff --git a/downloads-api/src/main/java/com/example/QuickstartApp.java b/downloads-api/src/main/java/com/example/QuickstartApp.java deleted file mode 100644 index ac1187ee..00000000 --- a/downloads-api/src/main/java/com/example/QuickstartApp.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example; - -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.http.javadsl.ConnectHttp; -import akka.http.javadsl.Http; -import akka.http.javadsl.ServerBinding; -import akka.http.javadsl.model.HttpRequest; -import akka.http.javadsl.model.HttpResponse; -import akka.http.javadsl.server.Route; -import akka.stream.Materializer; -import akka.stream.javadsl.Flow; -import akka.actor.typed.javadsl.Adapter; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.ActorSystem; - -import java.net.InetSocketAddress; -import java.util.concurrent.CompletionStage; - -//#main-class -public class QuickstartApp { - // #start-http-server - static void startHttpServer(Route route, ActorSystem system) { - CompletionStage futureBinding = - Http.get(system).newServerAt("localhost", 8080).bind(route); - - futureBinding.whenComplete((binding, exception) -> { - if (binding != null) { - InetSocketAddress address = binding.localAddress(); - system.log().info("Server online at http://{}:{}/", - address.getHostString(), - address.getPort()); - } else { - system.log().error("Failed to bind HTTP endpoint, terminating system", exception); - system.terminate(); - } - }); - } - // #start-http-server - - public static void main(String[] args) throws Exception { - //#server-bootstrapping - Behavior rootBehavior = Behaviors.setup(context -> { - ActorRef userRegistryActor = - context.spawn(UserRegistry.create(), "UserRegistry"); - - UserRoutes userRoutes = new UserRoutes(context.getSystem(), userRegistryActor); - startHttpServer(userRoutes.userRoutes(), context.getSystem()); - - return Behaviors.empty(); - }); - - // boot up server using the route as defined below - ActorSystem.create(rootBehavior, "HelloAkkaHttpServer"); - //#server-bootstrapping - } - -} -//#main-class - - diff --git a/downloads-api/src/main/java/com/example/UserRegistry.java b/downloads-api/src/main/java/com/example/UserRegistry.java deleted file mode 100644 index c8d914d2..00000000 --- a/downloads-api/src/main/java/com/example/UserRegistry.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.example; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.AbstractBehavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Receive; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import java.util.*; - -//#user-registry-actor -public class UserRegistry extends AbstractBehavior { - - // actor protocol - interface Command {} - - public record GetUsers(ActorRef replyTo) implements Command { - } - - public record CreateUser(User user, ActorRef replyTo) implements Command { - } - - public record GetUserResponse(Optional maybeUser) { - } - - public record GetUser(String name, ActorRef replyTo) implements Command { - } - - - public record DeleteUser(String name, ActorRef replyTo) implements Command { - } - - - public record ActionPerformed(String description) implements Command { - } - - //#user-case-classes - public record User( - @JsonProperty("name") String name, - @JsonProperty("age") int age, - @JsonProperty("countryOfResidence") String countryOfResidence - ) { - @JsonCreator - public User {} - } - - public record Users(List users) { } - //#user-case-classes - - private final List users = new ArrayList<>(); - - private UserRegistry(ActorContext context) { - super(context); - } - - public static Behavior create() { - return Behaviors.setup(UserRegistry::new); - } - - @Override - public Receive createReceive() { - return newReceiveBuilder() - .onMessage(GetUsers.class, this::onGetUsers) - .onMessage(CreateUser.class, this::onCreateUser) - .onMessage(GetUser.class, this::onGetUser) - .onMessage(DeleteUser.class, this::onDeleteUser) - .build(); - } - - private Behavior onGetUsers(GetUsers command) { - // We must be careful not to send out users since it is mutable - // so for this response we need to make a defensive copy - command.replyTo.tell(new Users(List.copyOf(users))); - return this; - } - - private Behavior onCreateUser(CreateUser command) { - users.add(command.user); - command.replyTo.tell(new ActionPerformed(String.format("User %s created.", command.user.name))); - return this; - } - - private Behavior onGetUser(GetUser command) { - Optional maybeUser = users.stream() - .filter(user -> user.name.equals(command.name)) - .findFirst(); - command.replyTo.tell(new GetUserResponse(maybeUser)); - return this; - } - - private Behavior onDeleteUser(DeleteUser command) { - users.removeIf(user -> user.name.equals(command.name)); - command.replyTo.tell(new ActionPerformed(String.format("User %s deleted.", command.name))); - return this; - } - -} -//#user-registry-actor diff --git a/downloads-api/src/main/java/com/example/UserRoutes.java b/downloads-api/src/main/java/com/example/UserRoutes.java deleted file mode 100644 index 6eea10f3..00000000 --- a/downloads-api/src/main/java/com/example/UserRoutes.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.example; - -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.CompletionStage; - -import com.example.UserRegistry.User; -import akka.actor.typed.ActorRef; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Scheduler; -import akka.actor.typed.javadsl.AskPattern; -import akka.http.javadsl.marshallers.jackson.Jackson; - -import static akka.http.javadsl.server.Directives.*; - -import akka.http.javadsl.model.StatusCodes; -import akka.http.javadsl.server.PathMatchers; -import akka.http.javadsl.server.Route; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Routes can be defined in separated classes like shown in here - */ -//#user-routes-class -public class UserRoutes { - //#user-routes-class - private final static Logger log = LoggerFactory.getLogger(UserRoutes.class); - private final ActorRef userRegistryActor; - private final Duration askTimeout; - private final Scheduler scheduler; - - public UserRoutes(ActorSystem system, ActorRef userRegistryActor) { - this.userRegistryActor = userRegistryActor; - scheduler = system.scheduler(); - askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); - } - - private CompletionStage getUser(String name) { - return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.GetUser(name, ref), askTimeout, scheduler); - } - - private CompletionStage deleteUser(String name) { - return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.DeleteUser(name, ref), askTimeout, scheduler); - } - - private CompletionStage getUsers() { - return AskPattern.ask(userRegistryActor, UserRegistry.GetUsers::new, askTimeout, scheduler); - } - - private CompletionStage createUser(User user) { - return AskPattern.ask(userRegistryActor, ref -> new UserRegistry.CreateUser(user, ref), askTimeout, scheduler); - } - - /** - * This method creates one route (of possibly many more that will be part of your Web App) - */ - //#all-routes - public Route userRoutes() { - return pathPrefix("users", () -> - concat( - //#users-get-delete - pathEnd(() -> - concat( - get(() -> - onSuccess(getUsers(), - users -> complete(StatusCodes.OK, users, Jackson.marshaller()) - ) - ), - post(() -> - entity( - Jackson.unmarshaller(User.class), - user -> - onSuccess(createUser(user), performed -> { - log.info("Create result: {}", performed.description()); - return complete(StatusCodes.CREATED, performed, Jackson.marshaller()); - }) - ) - ) - ) - ), - //#users-get-delete - //#users-get-post - path(PathMatchers.segment(), (String name) -> - concat( - get(() -> - //#retrieve-user-info - rejectEmptyResponse(() -> - onSuccess(getUser(name), performed -> - complete(StatusCodes.OK, performed.maybeUser(), Jackson.marshaller()) - ) - ) - //#retrieve-user-info - ), - delete(() -> - //#users-delete-logic - onSuccess(deleteUser(name), performed -> { - log.info("Delete result: {}", performed.description()); - return complete(StatusCodes.OK, performed, Jackson.marshaller()); - } - ) - //#users-delete-logic - ) - ) - ) - //#users-get-post - ) - ); - } - //#all-routes - -} diff --git a/downloads-api/src/main/java/module-info.java b/downloads-api/src/main/java/module-info.java deleted file mode 100644 index e4c4aee5..00000000 --- a/downloads-api/src/main/java/module-info.java +++ /dev/null @@ -1,17 +0,0 @@ -module org.spongepowered.downloads.app { - exports org.spongepowered.downloads.api; - - requires akka.actor.typed; - requires akka.http; - requires akka.http.core; - requires akka.stream; - requires akka.actor; - requires com.fasterxml.jackson.annotation; - requires akka.http.marshallers.jackson; - requires org.slf4j; - requires org.hibernate.orm.core; - requires java.persistence; - requires io.vavr; - requires com.fasterxml.jackson.databind; - requires maven.artifact; -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java deleted file mode 100644 index a1a4cb4c..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/api/Artifact.java +++ /dev/null @@ -1,46 +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.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import java.net.URI; -import java.util.Optional; - -@JsonSerialize -public final record Artifact( - @JsonProperty Optional classifier, - @JsonProperty URI downloadUrl, - @JsonProperty String md5, - @JsonProperty String sha1, - @JsonProperty String extension -) { - @JsonCreator - public Artifact { - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java deleted file mode 100644 index a3b29fe8..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java +++ /dev/null @@ -1,42 +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.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -@JsonDeserialize -public final record ArtifactCollection( - @JsonProperty("assets") List components, - @JsonProperty("coordinates") MavenCoordinates coordinates -) { - - @JsonCreator - public ArtifactCollection { - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java deleted file mode 100644 index b9f11573..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCoordinates.java +++ /dev/null @@ -1,66 +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.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.util.StringJoiner; - -@JsonDeserialize -public record ArtifactCoordinates( - @JsonProperty(required = true) String groupId, - @JsonProperty(required = true) String artifactId -) { - @JsonCreator - public ArtifactCoordinates { - } - - public MavenCoordinates version(String version) { - return MavenCoordinates.parse( - new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); - } - - public String asMavenString() { - return this.groupId() + ":" + this.artifactId(); - } - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String groupId() { - return groupId; - } - - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String artifactId() { - return artifactId; - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java deleted file mode 100644 index 4efe1286..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java +++ /dev/null @@ -1,42 +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.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record Group( - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String website -) { - - @JsonCreator - public Group { - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java deleted file mode 100644 index cab9ae7a..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/api/MavenCoordinates.java +++ /dev/null @@ -1,192 +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.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -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; -import java.util.regex.Pattern; - -@JsonDeserialize -public final class MavenCoordinates implements Comparable { - - private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String groupId; - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String artifactId; - /** - * The version of an artifact, as defined by the Apache Maven documentation. This is - * traditionally specified as a Maven repository searchable version string, such as - * {@code 1.0.0-SNAPSHOT}. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String version; - - @JsonIgnore - public final VersionType versionType; - - @JsonIgnore - private final ComparableVersion mavenVersion; - - /** - * Parses a set of maven formatted coordinates as per - * Apache - * Maven's documentation. - * - * @param coordinates The coordinates delimited by `:` - * @return A parsed set of MavenCoordinates - */ - public static MavenCoordinates parse(final String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - return new MavenCoordinates(groupId, artifactId, version); - } - - public MavenCoordinates(String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonCreator - public MavenCoordinates( - @JsonProperty("groupId") final String groupId, - @JsonProperty("artifactId") final String artifactId, - @JsonProperty("version") final String version - ) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonIgnore - public String asStandardCoordinates() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.versionType.asStandardVersionString(this.version)) - .toString(); - } - - @JsonIgnore - public boolean isSnapshot() { - return this.versionType.isSnapshot(); - } - - @JsonIgnore - public ArtifactCoordinates asArtifactCoordinates() { - return new ArtifactCoordinates(this.groupId, this.artifactId); - } - - @Override - public String toString() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.version) - .toString(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final MavenCoordinates that = (MavenCoordinates) o; - return Objects.equals(this.groupId, that.groupId) && Objects.equals( - this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.artifactId, this.version); - } - - @Override - public int compareTo(final MavenCoordinates o) { - final var group = this.groupId.compareTo(o.groupId); - if (group != 0) { - return group; - } - final var artifact = this.artifactId.compareTo(o.artifactId); - if (artifact != 0) { - return artifact; - } - return this.mavenVersion.compareTo(o.mavenVersion); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java b/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java deleted file mode 100644 index f5e37946..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/app/SystemOfADownloadsApp.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.spongepowered.downloads.app; - -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.http.javadsl.Http; -import akka.http.javadsl.ServerBinding; -import akka.http.javadsl.server.Route; -import org.spongepowered.downloads.artifacts.ArtifactQueries; -import org.spongepowered.downloads.artifacts.ArtifactRoutes; - -import java.net.InetSocketAddress; -import java.util.concurrent.CompletionStage; - -public final class SystemOfADownloadsApp { - - public static void main(final String[] args) { - //#server-bootstrapping - Behavior rootBehavior = Behaviors.setup(context -> { - ActorRef userRegistryActor = - context.spawn(ArtifactQueries.create(), "ArtifactQueries"); - - ArtifactRoutes userRoutes = new ArtifactRoutes(context.getSystem(), userRegistryActor); - startHttpServer(userRoutes.artifactRoutes(), context.getSystem()); - - return Behaviors.empty(); - }); - - // boot up server using the route as defined below - ActorSystem.create(rootBehavior, "HelloAkkaHttpServer"); - //#server-bootstrapping - } - - static void startHttpServer(Route route, ActorSystem system) { - CompletionStage futureBinding = - Http.get(system).newServerAt("localhost", 8080).bind(route); - - futureBinding.whenComplete((binding, exception) -> { - if (binding != null) { - InetSocketAddress address = binding.localAddress(); - system.log().info("Server online at http://{}:{}/", - address.getHostString(), - address.getPort()); - } else { - system.log().error("Failed to bind HTTP endpoint, terminating system", exception); - system.terminate(); - } - }); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java deleted file mode 100644 index a066952c..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactQueries.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.spongepowered.downloads.artifacts; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.AbstractBehavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Receive; -import org.spongepowered.downloads.artifacts.transport.GetArtifactDetailsResponse; -import org.spongepowered.downloads.artifacts.transport.GetArtifactsResponse; -import org.spongepowered.downloads.artifacts.transport.GroupResponse; -import org.spongepowered.downloads.artifacts.transport.GroupsResponse; - -public class ArtifactQueries extends AbstractBehavior { - - public static Behavior create() { - return Behaviors.receive(Command.class) - .build(); - } - - public ArtifactQueries(final ActorContext context) { - super(context); - } - - public sealed interface Command { - - record GetGroup( - String groupId, - ActorRef replyTo - ) implements Command { - } - - record GetArtifacts( - String groupId, - ActorRef replyTo - ) implements Command { - } - - record GetGroups( - ActorRef replyTo - ) implements Command { - } - - record GetArtifactDetails( - String groupId, - String artifactId, - ActorRef replyTo - ) implements Command { - } - - } - - - @Override - public Receive createReceive() { - return this.newReceiveBuilder() - .build(); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java deleted file mode 100644 index 1733a99c..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/ArtifactRoutes.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.spongepowered.downloads.artifacts; - -import static akka.http.javadsl.server.Directives.complete; -import static akka.http.javadsl.server.Directives.concat; -import static akka.http.javadsl.server.Directives.get; -import static akka.http.javadsl.server.Directives.onSuccess; -import static akka.http.javadsl.server.Directives.path; -import static akka.http.javadsl.server.Directives.pathPrefix; -import static akka.http.javadsl.server.Directives.rejectEmptyResponse; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Scheduler; -import akka.actor.typed.javadsl.AskPattern; -import akka.http.javadsl.marshallers.jackson.Jackson; -import akka.http.javadsl.model.StatusCodes; -import akka.http.javadsl.server.PathMatchers; -import akka.http.javadsl.server.Route; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.spongepowered.downloads.artifacts.transport.GetArtifactsResponse; -import org.spongepowered.downloads.artifacts.transport.GroupResponse; -import org.spongepowered.downloads.artifacts.transport.GroupsResponse; - -import java.time.Duration; -import java.util.concurrent.CompletionStage; - -public final class ArtifactRoutes { - - public static final Logger logger = LoggerFactory.getLogger(ArtifactRoutes.class); - - private final ActorRef artifactQueries; - private final Duration askTimeout; - private final Scheduler scheduler; - - public ArtifactRoutes(ActorSystem system, ActorRef artifactQueries) { - this.artifactQueries = artifactQueries; - scheduler = system.scheduler(); - askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); - } - - private CompletionStage getGroups() { - return AskPattern.ask(artifactQueries, ArtifactQueries.Command.GetGroups::new, askTimeout, scheduler); - } - - private CompletionStage getGroup(String name) { - return AskPattern.ask( - artifactQueries, ref -> new ArtifactQueries.Command.GetGroup(name, ref), askTimeout, scheduler); - } - - private CompletionStage getArtifacts(String name) { - return AskPattern.ask( - artifactQueries, ref -> new ArtifactQueries.Command.GetArtifacts(name, ref), askTimeout, scheduler); - } - - private Route getGroup() { - return get(() -> onSuccess(getGroups(), groups -> - complete(StatusCodes.OK, groups, Jackson.marshaller()) - )); - } - - /** - * This method creates one route (of possibly many more that will be part of your Web App) - */ - public Route artifactRoutes() { - // v1/groups - return pathPrefix("groups", () -> - concat( - getGroup(), - // v1/groups/:groupId/ - path(PathMatchers.segment(), (String groupId) -> - concat( - get(() -> rejectEmptyResponse(() -> - onSuccess(getGroup(groupId), performed -> { - final var group = performed.group(); - logger.info("Groups get: {}", group); - if (group.isEmpty()) { - return complete(StatusCodes.BAD_REQUEST, "group not found"); - } - return complete(StatusCodes.OK, group, Jackson.marshaller()); - }))), - // v1/groups/:groupId/artifacts - pathPrefix("artifacts", () -> get(() -> rejectEmptyResponse(() -> - onSuccess(getArtifacts(groupId), performed -> { - logger.info("Get result: {}", performed.artifactIds()); - return complete(StatusCodes.OK, performed, Jackson.marshaller()); - }))) - )) - ) - )); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java deleted file mode 100644 index 7fad1598..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactDetails.java +++ /dev/null @@ -1,128 +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.transport; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.control.Either; -import io.vavr.control.Try; - -import java.net.URL; - -public final class ArtifactDetails { - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Update.Website.class, - name = "website"), - @JsonSubTypes.Type(value = Update.DisplayName.class, - name = "displayName"), - @JsonSubTypes.Type(value = Update.Issues.class, - name = "issues"), - @JsonSubTypes.Type(value = Update.GitRepository.class, - name = "gitRepository"), - }) - @JsonDeserialize - public sealed interface Update { - - Either validate(); - - record Website( - @JsonProperty(required = true) String website - ) implements Update { - - @JsonCreator - public Website { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.website())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.website()))); - } - } - - record DisplayName( - @JsonProperty(required = true) String display - ) implements Update { - - @JsonCreator - public DisplayName { - } - - @Override - public Either validate() { - return Either.right(this.display.trim()); - } - } - - record Issues( - @JsonProperty(required = true) String issues - ) implements Update { - @JsonCreator - public Issues { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.issues())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.issues()))); - } - } - - record GitRepository( - @JsonProperty(required = true) String gitRepo - ) implements Update { - - @JsonCreator - public GitRepository { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.gitRepo())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.gitRepo()))); - } - } - } - - @JsonSerialize - public record Response( - String name, - String displayName, - String website, - String issues, - String gitRepo - ) { - - } - - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java deleted file mode 100644 index 4ab6dcc7..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/ArtifactRegistration.java +++ /dev/null @@ -1,82 +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.transport; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -public final class ArtifactRegistration { - - @JsonSerialize - public record RegisterArtifact( - @JsonProperty(required = true) String artifactId, - @JsonProperty(required = true) String displayName - ) { - - @JsonCreator - public RegisterArtifact(final String artifactId, final String displayName) { - this.artifactId = artifactId; - this.displayName = displayName; - } - - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, - name = "UnknownGroup"), - @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, - name = "RegisteredArtifact"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, - name = "AlreadyRegistered"), - }) - public sealed interface Response extends Jsonable { - - @JsonSerialize - record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { - - } - - @JsonSerialize - record ArtifactAlreadyRegistered( - @JsonProperty String artifactName, - @JsonProperty String groupId - ) implements Response { - - } - - @JsonSerialize - record GroupMissing(@JsonProperty("groupId") String s) implements Response { - - } - - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java deleted file mode 100644 index 065628bf..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactDetailsResponse.java +++ /dev/null @@ -1,49 +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.transport; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.Map; -import io.vavr.collection.SortedSet; -import org.spongepowered.downloads.api.ArtifactCoordinates; - -import java.io.Serializable; -import java.util.Optional; - -@JsonSerialize -public record GetArtifactDetailsResponse(Optional maybeArtifact) { - - @JsonSerialize - record RetrievedArtifact( - ArtifactCoordinates coordinates, - String displayName, - String website, - String gitRepository, - String issues, - Map> tags - ) implements Serializable { - - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.java deleted file mode 100644 index f22f221b..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GetArtifactsResponse.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.transport; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; - -import java.io.Serializable; - - -@JsonSerialize -public record GetArtifactsResponse(@JsonProperty List artifactIds) implements Serializable { - @JsonCreator - public GetArtifactsResponse {} -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java deleted file mode 100644 index e7815bb9..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupRegistration.java +++ /dev/null @@ -1,56 +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.transport; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.Group; - -public final class GroupRegistration { - - @JsonDeserialize - public record RegisterGroupRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String website - ) { - - @JsonCreator - public RegisterGroupRequest { } - - } - - public interface Response extends Jsonable { - - record GroupAlreadyRegistered(String groupNameRequested) implements Response { - } - - record GroupRegistered(Group group) implements Response { - - } - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java deleted file mode 100644 index 2a3e2790..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupResponse.java +++ /dev/null @@ -1,36 +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.transport; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.spongepowered.downloads.api.Group; - -import java.io.Serializable; -import java.util.Optional; - -@JsonSerialize -public record GroupResponse(Optional group) implements Serializable { - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java b/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java deleted file mode 100644 index c2947a8a..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/artifacts/transport/GroupsResponse.java +++ /dev/null @@ -1,38 +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.transport; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; -import org.spongepowered.downloads.api.Group; - -@JsonSerialize -public record GroupsResponse( - @JsonProperty("groups") - List groups -) { - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java deleted file mode 100644 index 7d35e43f..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/routes/VersionRoutes.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.spongepowered.downloads.routes; - -public class VersionRoutes { -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java deleted file mode 100644 index b4723af2..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionQueries.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.spongepowered.downloads.versions; - -import akka.actor.typed.ActorRef; -import org.spongepowered.downloads.api.ArtifactCoordinates; -import org.spongepowered.downloads.api.MavenCoordinates; -import org.spongepowered.downloads.versions.transport.QueryLatest; -import org.spongepowered.downloads.versions.transport.QueryVersions; - -class VersionQueries { - - sealed interface Command { - record GetVersion(MavenCoordinates coordinates, ActorRef replyTo) implements Command {} - record GetVersions(ArtifactCoordinates coordinates, ActorRef replyTo) implements Command {} - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java deleted file mode 100644 index 0a44794b..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/VersionRoutes.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.spongepowered.downloads.versions; - - -import static akka.http.javadsl.server.Directives.complete; -import static akka.http.javadsl.server.Directives.concat; -import static akka.http.javadsl.server.Directives.get; -import static akka.http.javadsl.server.Directives.onSuccess; -import static akka.http.javadsl.server.Directives.pathPrefix; -import static akka.http.javadsl.server.PathMatchers.segment; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Scheduler; -import akka.actor.typed.javadsl.AskPattern; -import akka.http.javadsl.marshallers.jackson.Jackson; -import akka.http.javadsl.model.StatusCodes; -import akka.http.javadsl.server.PathMatchers; -import akka.http.javadsl.server.Route; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.spongepowered.downloads.api.ArtifactCoordinates; -import org.spongepowered.downloads.versions.transport.QueryVersions; - -import java.time.Duration; -import java.util.concurrent.CompletionStage; - -public final class VersionRoutes { - - public static final Logger logger = LoggerFactory.getLogger( - org.spongepowered.downloads.artifacts.ArtifactRoutes.class); - - private final ActorRef artifactQueries; - private final Duration askTimeout; - private final Scheduler scheduler; - - public VersionRoutes(ActorSystem system, ActorRef artifactQueries) { - this.artifactQueries = artifactQueries; - scheduler = system.scheduler(); - askTimeout = system.settings().config().getDuration("my-app.routes.ask-timeout"); - } - - CompletionStage getVersions(ArtifactCoordinates coordinates) { - return AskPattern.ask(this.artifactQueries, ref -> new VersionQueries.Command.GetVersions( - coordinates, ref - ), this.askTimeout, this.scheduler); - } - - - /** - * This method creates one route (of possibly many more that will be part of your Web App) - */ - public Route artifactRoutes() { - // v1/groups - return pathPrefix( - segment("groups").slash(segment()).slash(segment("artifacts").slash(segment())), - (groupId, artifactId) -> - concat( - get(() -> onSuccess(this.getVersions(new ArtifactCoordinates(groupId, artifactId)), (resp) -> { - if (resp.size() <= 0) { - return complete(StatusCodes.NOT_FOUND); - } - return complete(StatusCodes.OK, resp, Jackson.marshaller()); - })), - path(PathMatchers.segment("versions").slash(), version -> get(() -> - onSuccess(this.getVersions(), resp -> { - return complete(StatusCodes.OK, resp, Jackson.marshaller()); - }))) - - ) - ); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java deleted file mode 100644 index b8d61a97..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaTaggedVersion.java +++ /dev/null @@ -1,170 +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.versions.models; - -import org.hibernate.annotations.Immutable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; -import java.io.Serializable; -import java.util.Objects; - -@Entity(name = "TaggedVersion") -@Immutable -@Table(name = "versioned_tags", schema = "version") -@NamedQueries({ - @NamedQuery(name = "TaggedVersion.findAllForVersion", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId and view.mavenArtifactId = :artifactId and view.version = :version - """ - ), - @NamedQuery( - name = "TaggedVersion.findAllMatchingTagValues", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId - and view.mavenArtifactId = :artifactId - and view.tagName = :tagName - and view.tagValue like :tagValue - """ - ), - @NamedQuery( - name = "TaggedVersion.findMatchingTagValuesAndRecommendation", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId - and view.mavenArtifactId = :artifactId - and view.tagName = :tagName - and view.tagValue like :tagValue - and (view.versionView.recommended = :recommended or view.versionView.manuallyRecommended = :recommended) - """ - ) -}) -public class JpaTaggedVersion implements Serializable { - - @Id - @Column(name = "version_id", updatable = false) - private long versionId; - - @Id - @Column(updatable = false, name = "artifact_internal_id") - private long artifactId; - - @Id - @Column(name = "maven_group_id", updatable = false) - private String mavenGroupId; - - @Id - @Column(name = "maven_artifact_id", updatable = false) - private String mavenArtifactId; - - @Id - @Column(name = "maven_version", - updatable = false) - private String version; - - @Id - @Column(name = "tag_name", - updatable = false) - private String tagName; - - @Column(name = "tag_value", - updatable = false) - private String tagValue; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "maven_version", - referencedColumnName = "version", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "maven_group_id", - referencedColumnName = "group_id", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "maven_artifact_id", - referencedColumnName = "artifact_id", - nullable = false, - updatable = false, - insertable = false) - }) - private JpaVersionedArtifactView versionView; - - public String getTagName() { - return tagName; - } - - public String getTagValue() { - return tagValue; - } - - public MavenCoordinates asMavenCoordinates() { - return new MavenCoordinates(this.mavenGroupId, this.mavenArtifactId, this.version); - } - - public JpaVersionedArtifactView getVersion() { - return this.versionView; - } - - public String getMavenVersion() { - return this.version; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaTaggedVersion that = (JpaTaggedVersion) o; - return versionId == that.versionId && artifactId == that.artifactId && Objects.equals( - mavenGroupId, that.mavenGroupId) && Objects.equals( - mavenArtifactId, that.mavenArtifactId) && Objects.equals( - version, that.version) && Objects.equals(tagName, that.tagName) && Objects.equals( - tagValue, that.tagValue); - } - - @Override - public int hashCode() { - return Objects.hash(versionId, artifactId, mavenGroupId, mavenArtifactId, version, tagName, tagValue); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java deleted file mode 100644 index 5b9a2f40..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedArtifactView.java +++ /dev/null @@ -1,231 +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.versions.models; - -import io.vavr.Tuple; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.hibernate.annotations.Immutable; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.query.api.models.TagCollection; -import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.net.URI; -import java.util.Comparator; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -@Immutable -@Entity(name = "VersionedArtifactView") -@Table(name = "versioned_artifacts", - schema = "version") -@NamedQueries({ - @NamedQuery( - name = "VersionedArtifactView.count", - query = """ - select count(v) from VersionedArtifactView v - where v.groupId = :groupId and v.artifactId = :artifactId - """ - ), - @NamedQuery( - name = "VersionedArtifactView.recommendedCount", - query = """ - select count(v) from VersionedArtifactView v - where v.groupId = :groupId and v.artifactId = :artifactId and v.recommended = :recommended - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findByArtifact", - query = """ - select v from VersionedArtifactView v where v.artifactId = :artifactId and v.groupId = :groupId - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findByArtifactAndRecommendation", - query = """ - select v from VersionedArtifactView v - where v.artifactId = :artifactId and v.groupId = :groupId and (v.recommended = :recommended or v.manuallyRecommended = :recommended) - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findExplicitly", - query = """ - select v from VersionedArtifactView v - left join fetch v.tags - where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findFullVersionDetails", - query = """ - select v from VersionedArtifactView v - left join fetch v.tags - left join fetch v.assets - where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version - """ - ) -}) -public class JpaVersionedArtifactView implements Serializable { - - @Id - @Column(name = "artifact_id", - updatable = false) - private String artifactId; - - @Id - @Column(name = "group_id", - updatable = false) - private String groupId; - - @Id - @Column(name = "version", - updatable = false) - private String version; - - @Column(name = "recommended") - private boolean recommended; - - @Column(name = "manual_recommendation") - private boolean manuallyRecommended; - - @Column(name = "ordering") - private int ordering; - - @OneToMany( - targetEntity = JpaTaggedVersion.class, - fetch = FetchType.LAZY, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "versionView") - private Set tags; - - @OneToMany( - targetEntity = JpaVersionedAsset.class, - cascade = CascadeType.ALL, - fetch = FetchType.LAZY, - orphanRemoval = true, - mappedBy = "versionView" - ) - private Set assets; - - @OneToOne( - targetEntity = JpaVersionedChangelog.class, - mappedBy = "versionView", - fetch = FetchType.LAZY - ) - private JpaVersionedChangelog changelog; - - public Set getTags() { - return tags; - } - - public String version() { - return this.version; - } - - public Map getTagValues() { - final var results = this.getTags(); - final var tuple2Stream = results.stream().map( - taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); - return tuple2Stream - .collect(HashMap.collector()); - } - - public TagCollection asTagCollection() { - final var results = this.getTags(); - final var tuple2Stream = results.stream().map( - taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); - return new TagCollection(tuple2Stream - .collect(HashMap.collector()), this.recommended); - } - - public boolean isRecommended() { - return this.recommended || this.manuallyRecommended; - } - - public MavenCoordinates asMavenCoordinates() { - return new MavenCoordinates(this.groupId + ":" + this.artifactId + ":" + this.version); - } - - Set getAssets() { - return assets; - } - - public List asArtifactList() { - return this.getAssets() - .stream() - .map( - asset -> new Artifact( - Optional.ofNullable(asset.getClassifier()), - URI.create(asset.getDownloadUrl()), - new String(asset.getMd5()), - new String(asset.getSha1()), - asset.getExtension() - ) - ).collect(List.collector()) - .sorted(Comparator.comparing(artifact -> artifact.classifier().orElse(""))); - } - - public Optional asVersionedCommit() { - final var changelog = this.changelog; - if (changelog == null) { - return Optional.empty(); - } - return Optional.of(changelog.getChangelog()); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedArtifactView that = (JpaVersionedArtifactView) o; - return Objects.equals(artifactId, that.artifactId) && Objects.equals( - groupId, that.groupId) && Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(artifactId, groupId, version); - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java deleted file mode 100644 index 3f3cd1d4..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedAsset.java +++ /dev/null @@ -1,146 +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.versions.models; - -import org.hibernate.annotations.Immutable; -import org.hibernate.annotations.Type; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.IdClass; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.Lob; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.util.Arrays; -import java.util.Objects; - -@Immutable -@Entity(name = "VersionedAsset") -@Table(name = "artifact_versioned_assets", - schema = "version") -@IdClass(VersionedAssetID.class) -public class JpaVersionedAsset implements Serializable { - - @Id - @Column(name = "group_id") - private String groupId; - @Id - @Column(name = "artifact_id") - private String artifactId; - - @Id - @Column(name = "version") - private String version; - - @Id - @Column(name = "classifier") - private String classifier; - - @Id - @Column(name = "extension") - private String extension; - - @Column(name = "download_url") - private String downloadUrl; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "md5") - private byte[] md5; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "sha1") - private byte[] sha1; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "version", - referencedColumnName = "version", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "group_id", - referencedColumnName = "group_id", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "artifact_id", - referencedColumnName = "artifact_id", - nullable = false, - updatable = false, - insertable = false) - }) - private JpaVersionedArtifactView versionView; - - public String getClassifier() { - return classifier; - } - - public String getExtension() { - return extension; - } - - public String getDownloadUrl() { - return downloadUrl; - } - - public byte[] getMd5() { - return md5; - } - - public byte[] getSha1() { - return sha1; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedAsset that = (JpaVersionedAsset) o; - return Objects.equals(groupId, that.groupId) && Objects.equals( - artifactId, that.artifactId) && Objects.equals(version, that.version) && Objects.equals( - classifier, that.classifier) && Objects.equals(extension, that.extension) && Objects.equals( - downloadUrl, that.downloadUrl) && Arrays.equals(md5, that.md5) && Arrays.equals( - sha1, that.sha1) && Objects.equals(versionView, that.versionView); - } - - @Override - public int hashCode() { - int result = Objects.hash(groupId, artifactId, version, classifier, extension, downloadUrl, versionView); - result = 31 * result + Arrays.hashCode(md5); - result = 31 * result + Arrays.hashCode(sha1); - return result; - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java deleted file mode 100644 index 68396320..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/JpaVersionedChangelog.java +++ /dev/null @@ -1,140 +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.versions.models; - -import com.vladmihalcea.hibernate.type.json.JsonBinaryType; -import org.hibernate.annotations.Immutable; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.hibernate.annotations.TypeDefs; -import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.net.URL; -import java.util.Objects; - -@Immutable -@Entity(name = "VersionedChangelog") -@Table( - name = "versioned_changelogs", - schema = "version" -) -@TypeDefs({ - @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) -}) -public class JpaVersionedChangelog implements Serializable { - - @Id - @Column(name = "version_id", updatable = false) - private String versionId; - - @Id - @Column(name = "group_id", updatable = false) - private String groupId; - - @Id - @Column(name = "artifact_id", updatable = false) - private String artifactId; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false), - @JoinColumn(name = "group_id", referencedColumnName = "group_id", insertable = false, updatable = false), - @JoinColumn(name = "artifact_id", referencedColumnName = "artifact_id", insertable = false, updatable = false) - }) - private JpaVersionedArtifactView versionView; - - @Column(name = "commit_sha", nullable = false) - private String sha; - - @Type(type = "jsonb") - @Column(name = "changelog", columnDefinition = "jsonb") - private VersionedChangelog changelog; - - @Column(name = "repo") - private URL repo; - - @Column(name = "branch") - private String branch; - - public String getSha() { - return sha; - } - - public VersionedChangelog getChangelog() { - return changelog; - } - - public URL getRepo() { - return repo; - } - - public String getBranch() { - return branch; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedChangelog that = (JpaVersionedChangelog) o; - return Objects.equals(versionId, that.versionId) && Objects.equals( - groupId, that.groupId) && Objects.equals(artifactId, that.artifactId) && Objects.equals( - versionView, that.versionView) && Objects.equals(sha, that.sha) && Objects.equals( - changelog, that.changelog) && Objects.equals(repo, that.repo) && Objects.equals( - branch, that.branch); - } - - @Override - public int hashCode() { - return Objects.hash(versionId, groupId, artifactId, versionView, sha, changelog, repo, branch); - } - - @Override - public String toString() { - return "JpaVersionedChangelog{" + - "versionId='" + versionId + '\'' + - ", groupId='" + groupId + '\'' + - ", artifactId='" + artifactId + '\'' + - ", versionView=" + versionView + - ", sha='" + sha + '\'' + - ", changelog=" + changelog + - ", repo=" + repo + - ", branch='" + branch + '\'' + - '}'; - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java deleted file mode 100644 index 3156c74d..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedArtifactID.java +++ /dev/null @@ -1,59 +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.versions.models; - -import java.io.Serializable; -import java.util.Objects; - -public class VersionedArtifactID implements Serializable { - private String artifactId; - - private String groupId; - - private String version; - - public VersionedArtifactID(final String artifactId, final String groupId, final String version) { - this.artifactId = artifactId; - this.groupId = groupId; - this.version = version; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - VersionedArtifactID that = (VersionedArtifactID) o; - return artifactId.equals(that.artifactId) && groupId.equals(that.groupId) && version.equals(that.version); - } - - @Override - public int hashCode() { - return Objects.hash(artifactId, groupId, version); - } -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java deleted file mode 100644 index 7bbba094..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/models/VersionedAssetID.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.spongepowered.downloads.versions.models; - -import java.io.Serializable; - -public record VersionedAssetID( - String groupId, - String artifactId, - String version, - String classifier, - String extension - ) implements Serializable { -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java deleted file mode 100644 index 80d4a01d..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryLatest.java +++ /dev/null @@ -1,46 +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.versions.transport; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.util.Optional; - -public interface QueryLatest { - - @JsonSerialize - record VersionInfo(MavenCoordinates coordinates, - List assets, - Map tagValues, - Optional commit, - boolean recommended - ) { - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java deleted file mode 100644 index 2ac34a11..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/QueryVersions.java +++ /dev/null @@ -1,57 +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.versions.transport; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.api.Artifact; -import org.spongepowered.downloads.api.MavenCoordinates; - -import java.util.Optional; - -public interface QueryVersions { - - @JsonSerialize - record VersionInfo(@JsonProperty Map artifacts, int offset, int limit, int size) { - - @JsonCreator - public VersionInfo { - } - } - - @JsonSerialize - record VersionDetails( - @JsonProperty("coordinates") MavenCoordinates coordinates, - @JsonProperty("commit") Optional commit, - @JsonProperty("assets") List components, - @JsonProperty("tags") Map tagValues, - @JsonProperty("recommended") boolean recommended - ) { - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java deleted file mode 100644 index f740b7a8..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/TagCollection.java +++ /dev/null @@ -1,35 +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.versions.transport; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.Map; - -@JsonSerialize -public record TagCollection( - Map tagValues, - boolean recommended -) { -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java deleted file mode 100644 index efb8d8f0..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedChangelog.java +++ /dev/null @@ -1,65 +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.versions.transport; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -import java.net.URI; - -@JsonDeserialize -public final record VersionedChangelog( - List commits, - @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean processing -) { - - @JsonCreator - public VersionedChangelog { - } - - @JsonDeserialize - public final record IndexedCommit( - VersionedCommit commit, - List submoduleCommits - ) { - @JsonCreator - public IndexedCommit { - } - } - - @JsonDeserialize - public final record Submodule( - String name, - URI gitRepository, - List commits - ) { - @JsonCreator - public Submodule { - } - } - -} diff --git a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java b/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java deleted file mode 100644 index 869d6888..00000000 --- a/downloads-api/src/main/java/org/spongepowered/downloads/versions/transport/VersionedCommit.java +++ /dev/null @@ -1,69 +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.versions.transport; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.net.URI; -import java.time.ZonedDateTime; - -@JsonDeserialize -public record VersionedCommit( - String message, - String body, - String sha, - Author author, - Commiter commiter, - URI link, - ZonedDateTime commitDate -) { - - @JsonCreator - public VersionedCommit { - } - - @JsonDeserialize - public record Author( - String name, - String email - ) { - @JsonCreator - public Author { - } - } - - @JsonDeserialize - public record Commiter( - String name, - String email - ) { - @JsonCreator - public Commiter { - } - } -} - diff --git a/downloads-api/src/main/resources/application.conf b/downloads-api/src/main/resources/application.conf deleted file mode 100644 index acd17dfa..00000000 --- a/downloads-api/src/main/resources/application.conf +++ /dev/null @@ -1,6 +0,0 @@ -my-app { - routes { - # If ask takes more time than this to complete the request is failed - ask-timeout = 5s - } -} diff --git a/downloads-api/src/main/resources/logback.xml b/downloads-api/src/main/resources/logback.xml deleted file mode 100644 index b1fe9ae9..00000000 --- a/downloads-api/src/main/resources/logback.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - [%date{ISO8601}] [%level] [%logger] [%thread] [%X{akkaSource}] - %msg%n - - - - - 1024 - true - - - - - - - - diff --git a/downloads-api/src/test/java/com/example/UserRoutesTest.java b/downloads-api/src/test/java/com/example/UserRoutesTest.java deleted file mode 100644 index 02896cae..00000000 --- a/downloads-api/src/test/java/com/example/UserRoutesTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.example; - - -//#test-top -import akka.actor.typed.ActorRef; -import akka.http.javadsl.model.*; -import akka.http.javadsl.testkit.JUnitRouteTest; -import akka.http.javadsl.testkit.TestRoute; -import org.junit.*; -import org.junit.runners.MethodSorters; -import akka.http.javadsl.model.HttpRequest; -import akka.http.javadsl.model.StatusCodes; -import akka.actor.testkit.typed.javadsl.TestKitJunitResource; - - -//#set-up -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class UserRoutesTest extends JUnitRouteTest { - - @ClassRule - public static TestKitJunitResource testkit = new TestKitJunitResource(); - - //#test-top - // shared registry for all tests - private static ActorRef userRegistry; - private TestRoute appRoute; - - @BeforeClass - public static void beforeClass() { - userRegistry = testkit.spawn(UserRegistry.create()); - } - - @Before - public void before() { - UserRoutes userRoutes = new UserRoutes(testkit.system(), userRegistry); - appRoute = testRoute(userRoutes.userRoutes()); - } - - @AfterClass - public static void afterClass() { - testkit.stop(userRegistry); - } - - //#set-up - //#actual-test - @Test - public void test1NoUsers() { - appRoute.run(HttpRequest.GET("/users")) - .assertStatusCode(StatusCodes.OK) - .assertMediaType("application/json") - .assertEntity("{\"users\":[]}"); - } - - //#actual-test - //#testing-post - @Test - public void test2HandlePOST() { - appRoute.run(HttpRequest.POST("/users") - .withEntity(MediaTypes.APPLICATION_JSON.toContentType(), - "{\"name\": \"Kapi\", \"age\": 42, \"countryOfResidence\": \"jp\"}")) - .assertStatusCode(StatusCodes.CREATED) - .assertMediaType("application/json") - .assertEntity("{\"description\":\"User Kapi created.\"}"); - } - //#testing-post - - @Test - public void test3Remove() { - appRoute.run(HttpRequest.DELETE("/users/Kapi")) - .assertStatusCode(StatusCodes.OK) - .assertMediaType("application/json") - .assertEntity("{\"description\":\"User Kapi deleted.\"}"); - - } - //#set-up -} -//#set-up diff --git a/downloads-api/src/test/resources/application-test.conf b/downloads-api/src/test/resources/application-test.conf deleted file mode 100644 index 7f763324..00000000 --- a/downloads-api/src/test/resources/application-test.conf +++ /dev/null @@ -1,3 +0,0 @@ -include "application" - -# default config for tests, we just import the regular conf \ No newline at end of file diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 52b199d6..00000000 --- a/gradle.properties +++ /dev/null @@ -1,7 +0,0 @@ -micronautVersion=3.8.1 -akkaVersion =2.7.0 -scalaVersion=2.13 -akkaManagementVersion=1.2.0 -akkaProjection =1.3.1 -jacksonVersion = 2.14.2 -vavr = 0.10.4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..839385ba --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,46 @@ +[versions] +micronaut = "4.0.5" +scala = "2.13" +akka = "2.8.3" +jackson = "2.15.1" +maven_artifact = "3.8.5" +akkaManagementVersion = "1.4.1" +akkaProjection = "1.4.2" +akkaR2DBC = "1.1.1" +vavr = "0.10.4" +jakartaValidation = "3.0.2" + +[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 = { 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"} + +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"} +maven = { module = "org.apache.maven:maven-artifact", version.ref = "maven_artifact" } + +jakarta-validation = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakartaValidation"} + + +[bundles] +serder = ["jackson-core", "jackson-annotations", "jackson-databind"] +appSerder = ["jackson-databind", "jackson-annotations", "jackson-core", "akka-jackson"] +actors = ["akka-actor", "akka-cluster-typed", "akka-cluster-sharding"] +actorsPersistence = ["akka-persistence", "akka-projection", "akka-r2dbc", "postgres-r2dbc"] +akkaManagement = ["akka-discovery", "lightbend_bootstrap", "lightbend_management"] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e5832f090a2944b7473328c07c9755baa3196..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 39834 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 G$^QWPgI3l6 delta 38507 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 + + + + + + diff --git a/liquibase/changelog/changelog.xml b/liquibase/changelog/changelog.xml index 64c2215a..9d5a66c2 100644 --- a/liquibase/changelog/changelog.xml +++ b/liquibase/changelog/changelog.xml @@ -3,5 +3,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd"> - + diff --git a/liquibase/changelog/lagom/lagom_1_6.xml b/liquibase/changelog/lagom/lagom_1_6.xml deleted file mode 100644 index 746eef08..00000000 --- a/liquibase/changelog/lagom/lagom_1_6.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/project/build.properties b/project/build.properties deleted file mode 100644 index 3161d214..00000000 --- a/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=1.6.1 diff --git a/project/plugins.sbt b/project/plugins.sbt deleted file mode 100644 index a77de1ce..00000000 --- a/project/plugins.sbt +++ /dev/null @@ -1,8 +0,0 @@ -addSbtPlugin("com.lightbend.lagom" % "lagom-sbt-plugin" % "1.6.7") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.7.0") -addDependencyTreePlugin -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") - -resolvers += Resolver.jcenterRepo - -addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.10.0") diff --git a/server-auth/src/main/java/org/spongepowered/downloads/auth/AuthenticatedInternalService.java b/server-auth/src/main/java/org/spongepowered/downloads/auth/AuthenticatedInternalService.java deleted file mode 100644 index c5726e99..00000000 --- a/server-auth/src/main/java/org/spongepowered/downloads/auth/AuthenticatedInternalService.java +++ /dev/null @@ -1,63 +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.auth; - -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.server.HeaderServiceCall; -import com.lightbend.lagom.javadsl.server.ServerServiceCall; -import org.pac4j.core.profile.CommonProfile; -import org.pac4j.lagom.javadsl.SecuredService; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; - -import java.util.function.Function; - -public interface AuthenticatedInternalService extends SecuredService { - - @Override - default ServerServiceCall authorize( - final String clientName, - final String authorizerName, - final Function> serviceCall - ) { - return HeaderServiceCall.compose(requestHeader -> - requestHeader.getHeader(auth().internalHeaderKey()) - .filter(header -> header.equals(auth().internalHeaderSecret())) - .map(verified -> new InternalApplicationProfile()) - .map(serviceCall) - .orElseGet(() -> SecuredService.super.authorize(clientName, authorizerName, serviceCall)) - ); - } - - default ServiceCall authorizeInvoke( - final ServiceCall call - ) { - return call.handleRequestHeader(requestHeader -> requestHeader.withHeader( - this.auth().internalHeaderKey(), - this.auth().internalHeaderSecret() - )); - } - - AuthUtils auth(); -} diff --git a/server-auth/src/main/java/org/spongepowered/downloads/auth/InternalApplicationProfile.java b/server-auth/src/main/java/org/spongepowered/downloads/auth/InternalApplicationProfile.java deleted file mode 100644 index a1691f33..00000000 --- a/server-auth/src/main/java/org/spongepowered/downloads/auth/InternalApplicationProfile.java +++ /dev/null @@ -1,35 +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.auth; - -import org.pac4j.core.profile.CommonProfile; - -public class InternalApplicationProfile extends CommonProfile { - - public InternalApplicationProfile() { - this.setId("InternalApplication"); - } - -} diff --git a/server-auth/src/main/java/org/spongepowered/downloads/auth/SOADAuth.java b/server-auth/src/main/java/org/spongepowered/downloads/auth/SOADAuth.java deleted file mode 100644 index d132b022..00000000 --- a/server-auth/src/main/java/org/spongepowered/downloads/auth/SOADAuth.java +++ /dev/null @@ -1,32 +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.auth; - -/** - * Marker annotation to indicate that the returnable object should be the - * SOAD one. - */ -public @interface SOADAuth { -} diff --git a/server-auth/src/main/resources/reference.conf b/server-auth/src/main/resources/reference.conf deleted file mode 100644 index 6d98104e..00000000 --- a/server-auth/src/main/resources/reference.conf +++ /dev/null @@ -1,16 +0,0 @@ - -systemofadownload.auth.secrets { - # The encryption key must be at least 256 bits, so just populate a really - # big number here. Note that if multiple services are being used, the key - # must be the same across all the services, otherwise the tokens generated - # by the auth service will not be valid for other services. - encryption = "12345678901234567890123456789012" - signature = "12345678901234567890123456789012" - # For the webhook module, the specific secret when available, may well be - # deprecated in the future for a more registration-based webhook solution. - nexus-webhook = "" - # The internal header/secret key combination for inter-service - # communication. These should NOT remain the same in production! - internal-header = "some-header" - internal-secret = "some-secret" -} diff --git a/settings.gradle.kts b/settings.gradle.kts index bb2f00f9..50233302 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,20 +1,8 @@ -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } - plugins { - id("com.github.johnrengelman.shadow") version "7.1.2" - id("io.micronaut.application") version "3.7.0" - id("io.micronaut.test-resources") version "3.7.0" - } -} -rootProject.name="systemofadownload" +rootProject.name="SystemOfADownload" dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.PREFER_PROJECT) // needed for forge-loom, unfortunately repositories { mavenCentral() maven("https://repo.spongepowered.org/repository/maven-public/") { @@ -24,9 +12,11 @@ dependencyResolutionManagement { } include( + "akka", + "akka:testkit", "artifacts", "artifacts:api", "artifacts:worker", "artifacts:server", - "artifacts:events") -include("akka") + "artifacts:events", + ) diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/MavenConstants.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/MavenConstants.java deleted file mode 100644 index 4c8a2bcd..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/MavenConstants.java +++ /dev/null @@ -1,29 +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.maven; - -public final class MavenConstants { - public static final String MAVEN_METADATA_FILE = "maven-metadata.xml"; -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/ArtifactMavenMetadata.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/ArtifactMavenMetadata.java deleted file mode 100644 index dd18167c..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/ArtifactMavenMetadata.java +++ /dev/null @@ -1,86 +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.maven.artifact; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.util.Objects; - -@JsonDeserialize -@JsonIgnoreProperties(value = "modelVersion", ignoreUnknown = true) -public final class ArtifactMavenMetadata { - private final String groupId; - private final String artifactId; - private final Versioning versioning; - - @JsonCreator - public ArtifactMavenMetadata( - @JsonProperty("groupId") final String groupId, - @JsonProperty("artifactId") final String artifactId, - @JsonProperty("versioning") final Versioning versioning - ) { - this.groupId = groupId; - this.artifactId = artifactId; - this.versioning = versioning; - } - - public String groupId() { - return this.groupId; - } - - public String artifactId() { - return artifactId; - } - - public Versioning versioning() { - return this.versioning; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (ArtifactMavenMetadata) obj; - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.versioning, that.versioning); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.versioning); - } - - @Override - public String toString() { - return "ArtifactMavenMetadata[" + - "groupId=" + this.groupId + ", " + - "versioning=" + this.versioning + ']'; - } - - -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/Versioning.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/Versioning.java deleted file mode 100644 index 3153c06f..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/artifact/Versioning.java +++ /dev/null @@ -1,90 +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.maven.artifact; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -import java.util.Objects; -import java.util.Optional; - -@JsonDeserialize -public final class Versioning { - public final String latest; - public final String release; - public final String lastUpdated; - public final List versions; - - public Versioning() { - this.latest = ""; - this.release = ""; - this.lastUpdated = ""; - this.versions = List.empty(); - } - - @JsonCreator - public Versioning( - @JsonProperty("latest") final String latest, - @JsonProperty("release") final String release, - @JsonProperty("lastUpdated") String lastUpdated, - @JsonProperty("versions") List versions - ) { - this.latest = latest == null ? "" : latest; - this.release = release == null ? "" : release; - this.lastUpdated = lastUpdated; - this.versions = versions; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (Versioning) obj; - return Objects.equals(this.latest, that.latest) && - Objects.equals(this.release, that.release) && - Objects.equals(this.lastUpdated, that.lastUpdated) && - Objects.equals(this.versions, that.versions); - } - - @Override - public int hashCode() { - return Objects.hash(this.latest, this.release, this.lastUpdated, this.versions); - } - - @Override - public String toString() { - return "Versioning[" + - "latest=" + this.latest + ", " + - "release=" + this.release + ", " + - "lastUpdated=" + this.lastUpdated + ", " + - "versions=" + this.versions+ ']'; - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/Snapshot.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/Snapshot.java deleted file mode 100644 index 5936ecc7..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/Snapshot.java +++ /dev/null @@ -1,36 +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.maven.snapshot; - -public class Snapshot { - - public final String timestamp; - public final int buildNumber; - - public Snapshot(final String timestamp, final int buildNumber) { - this.timestamp = timestamp; - this.buildNumber = buildNumber; - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotAsset.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotAsset.java deleted file mode 100644 index 299e138c..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotAsset.java +++ /dev/null @@ -1,86 +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.maven.snapshot; - -import java.util.Objects; - -public final class SnapshotAsset { - private final String classifier; - private final String extension; - private final String value; - private final String updated; - - SnapshotAsset(final String classifier, final String extension, final String value, final String updated) { - this.classifier = classifier; - this.extension = extension; - this.value = value; - this.updated = updated; - } - - public String classifier() { - return this.classifier; - } - - public String extension() { - return this.extension; - } - - public String value() { - return this.value; - } - - public String updated() { - return this.updated; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (SnapshotAsset) obj; - return Objects.equals(this.classifier, that.classifier) && - Objects.equals(this.extension, that.extension) && - Objects.equals(this.value, that.value) && - Objects.equals(this.updated, that.updated); - } - - @Override - public int hashCode() { - return Objects.hash(this.classifier, this.extension, this.value, this.updated); - } - - @Override - public String toString() { - return "SnapshotAsset[" + - "classifier=" + this.classifier + ", " + - "extension=" + this.extension + ", " + - "value=" + this.value + ", " + - "updated=" + this.updated + ']'; - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotMetadata.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotMetadata.java deleted file mode 100644 index f78d43f6..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotMetadata.java +++ /dev/null @@ -1,42 +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.maven.snapshot; - -public final class SnapshotMetadata { - public final String groupId; - public final String artifactId; - public final String version; - public final SnapshotVersioning versioning; - - public SnapshotMetadata( - final String groupId, final String artifactId, final String version, - final SnapshotVersioning versioning - ) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versioning = versioning; - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotVersioning.java b/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotVersioning.java deleted file mode 100644 index b5106301..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/maven/snapshot/SnapshotVersioning.java +++ /dev/null @@ -1,47 +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.maven.snapshot; - -import io.vavr.collection.List; - -/** - * Represents a snapshot versioned maven metadata xml that SOAD will use to represent - * possible artifacts of snapshots. Note that due to the implicit requirements of - */ -public class SnapshotVersioning { - - public final Snapshot snapshot; - public final String lastUpdated; - public final List snapshotVersions; - - public SnapshotVersioning( - final Snapshot snapshot, final String lastUpdated, - final List snapshotVersions - ) { - this.snapshot = snapshot; - this.lastUpdated = lastUpdated; - this.snapshotVersions = snapshotVersions; - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/AssetSearchResponse.java b/sonatype/src/main/java/org/spongepowered/downloads/sonatype/AssetSearchResponse.java deleted file mode 100644 index 5868b14c..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/AssetSearchResponse.java +++ /dev/null @@ -1,43 +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.sonatype; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -import java.util.Optional; - -@JsonDeserialize -public record AssetSearchResponse( - @JsonProperty Optional continuationToken, - @JsonProperty(required = true) List items -) { - - @JsonCreator - public AssetSearchResponse { - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/Component.java b/sonatype/src/main/java/org/spongepowered/downloads/sonatype/Component.java deleted file mode 100644 index bce10302..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/Component.java +++ /dev/null @@ -1,111 +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.sonatype; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; - -@JsonDeserialize -@JsonSerialize -public record Component(String id, String repository, String format, - String group, String name, String version, - List assets) { - @JsonCreator - public Component( - @JsonProperty(value = "id", - required = true) final String id, - @JsonProperty(value = "repository", - required = true) final String repository, - @JsonProperty(value = "format", - required = true) final String format, - @JsonProperty(value = "group", - required = true) final String group, - @JsonProperty(value = "name", - required = true) final String name, - @JsonProperty(value = "version", - required = true) final String version, - @JsonProperty(value = "assets", - required = true) final List assets - ) { - this.id = id; - this.repository = repository; - this.format = format; - this.group = group; - this.name = name; - this.version = version; - this.assets = assets; - } - - @JsonDeserialize - public static record Asset( - @JsonProperty(value = "downloadUrl", - required = true) String downloadUrl, - @JsonProperty(value = "path", - required = true) String path, - @JsonProperty(value = "id", - required = true) String id, - @JsonProperty(value = "repository", - required = true) String repository, - @JsonProperty(value = "format", - required = true) String format, - @JsonProperty(value = "checksum", - required = true) Checksum checksum, - @JsonProperty(value = "contentType", - required = true) String contentType, - @JsonProperty(value = "lastModified", - required = true) String lastModified, - @JsonProperty(value = "maven2") Maven2 mavenData - ) { - @JsonCreator - public Asset { - } - } - - public static record Maven2( - @JsonProperty(value = "extension") String extension, - @JsonProperty(value = "groupId") String groupId, - @JsonProperty(value = "classifier") String classifier, - @JsonProperty(value = "artifactId") String artifactId, - @JsonProperty(value = "version") String version - ) { - - } - - @JsonDeserialize - public static record Checksum( - @JsonProperty(value = "sha1") String sha1, - @JsonProperty(value = "sha256") String sha256, - @JsonProperty(value = "sha512") String sha512, - @JsonProperty(value = "md5") String md5 - ) { - @JsonCreator - public Checksum { - } - - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/ComponentSearchResponse.java b/sonatype/src/main/java/org/spongepowered/downloads/sonatype/ComponentSearchResponse.java deleted file mode 100644 index 877692f1..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/ComponentSearchResponse.java +++ /dev/null @@ -1,140 +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.sonatype; - -import io.vavr.collection.List; - -import java.util.Objects; -import java.util.Optional; - -public final class ComponentSearchResponse { - private final List items; - private final Optional continuationToken; - - public ComponentSearchResponse(final List items, final Optional continuationToken) { - this.items = items; - this.continuationToken = continuationToken; - } - - public List items() { - return this.items; - } - - public Optional continuationToken() { - return this.continuationToken; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (ComponentSearchResponse) obj; - return Objects.equals(this.items, that.items) && - Objects.equals(this.continuationToken, that.continuationToken); - } - - @Override - public int hashCode() { - return Objects.hash(this.items, this.continuationToken); - } - - @Override - public String toString() { - return "ComponentSearchResponse[" + - "items=" + this.items + ", " + - "continuationToken=" + this.continuationToken + ']'; - } - - public final static class Item { - private final String id; - private final String repository; - private final String format; - private final String group; - private final String name; - private final String version; - - public Item(final String id, final String repository, final String format, final String group, final String name, final String version) { - this.id = id; - this.repository = repository; - this.format = format; - this.group = group; - this.name = name; - this.version = version; - } - - public String id() { - return this.id; - } - - public String repository() { - return this.repository; - } - - public String format() { - return this.format; - } - - public String group() { - return this.group; - } - - public String name() { - return this.name; - } - - public String version() { - return this.version; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (Item) obj; - return Objects.equals(this.id, that.id) && - Objects.equals(this.repository, that.repository) && - Objects.equals(this.format, that.format) && - Objects.equals(this.group, that.group) && - Objects.equals(this.name, that.name) && - Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.id, this.repository, this.format, this.group, this.name, this.version); - } - - @Override - public String toString() { - return "Item[" + - "id=" + this.id + ", " + - "repository=" + this.repository + ", " + - "format=" + this.format + ", " + - "group=" + this.group + ", " + - "name=" + this.name + ", " + - "version=" + this.version + ']'; - } - } -} diff --git a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/MavenPom.java b/sonatype/src/main/java/org/spongepowered/downloads/sonatype/MavenPom.java deleted file mode 100644 index 6c8a2a27..00000000 --- a/sonatype/src/main/java/org/spongepowered/downloads/sonatype/MavenPom.java +++ /dev/null @@ -1,82 +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.sonatype; - -import java.util.Objects; - -public final class MavenPom { - private final String groupId; - private final String artifactId; - private final String version; - private final String name; - - public MavenPom(final String groupId, final String artifactId, final String version, final String name) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.name = name; - } - - public String groupId() { - return this.groupId; - } - - public String artifactId() { - return this.artifactId; - } - - public String version() { - return this.version; - } - - public String name() { - return this.name; - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - final var that = (MavenPom) obj; - return Objects.equals(this.groupId, that.groupId) && - Objects.equals(this.artifactId, that.artifactId) && - Objects.equals(this.version, that.version) && - Objects.equals(this.name, that.name); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.artifactId, this.version, this.name); - } - - @Override - public String toString() { - return "MavenPom[" + - "groupId=" + this.groupId + ", " + - "artifactId=" + this.artifactId + ", " + - "version=" + this.version + ", " + - "name=" + this.name + ']'; - } -} diff --git a/sonatype/src/test/java/org/spongepowered/downloads/maven/MavenMetadataTest.java b/sonatype/src/test/java/org/spongepowered/downloads/maven/MavenMetadataTest.java deleted file mode 100644 index 46e9bba3..00000000 --- a/sonatype/src/test/java/org/spongepowered/downloads/maven/MavenMetadataTest.java +++ /dev/null @@ -1,95 +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.maven; - - -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import io.vavr.collection.List; -import io.vavr.jackson.datatype.VavrModule; -import org.junit.jupiter.api.Test; -import org.spongepowered.downloads.maven.artifact.ArtifactMavenMetadata; -import org.spongepowered.downloads.maven.artifact.Versioning; - -import java.io.IOException; - -public final class MavenMetadataTest { - - @Test - public void TestMavenMetadataDeserialization() throws IOException { - final var mapper = new XmlMapper(); - mapper.registerModule(new VavrModule()); - final var mavenMetadataFile = getClass().getClassLoader().getResourceAsStream("maven-metadata-example.xml"); - final var artifactMavenMetadata = mapper.readValue(mavenMetadataFile, ArtifactMavenMetadata.class); - // assertions - assert "spongeapi".equals(artifactMavenMetadata.artifactId()); - assert "org.spongepowered".equals(artifactMavenMetadata.groupId()); - final var existingVersions = List.of( - "1.0.0-SNAPSHOT", - "1.0", - "1.1-SNAPSHOT", - "2.0", - "2.1-SNAPSHOT", - "3.0.0", - "3.0.1-SNAPSHOT", - "3.0.1-indev", - "3.1.0-SNAPSHOT", - "3.1.0", - "4.0.0-SNAPSHOT", - "4.0.0", - "4.0.1", - "4.0.2", - "4.0.3", - "4.1.0-SNAPSHOT", - "4.1.0", - "4.2.0-SNAPSHOT", - "5.0.0-SNAPSHOT", - "5.0.0", - "5.1.0-SNAPSHOT", - "5.1.0", - "5.2.0-SNAPSHOT", - "6.0.0-SNAPSHOT", - "6.0.0", - "6.1.0-SNAPSHOT", - "7.0.0-SNAPSHOT", - "7.0.0", - "7.1.0-SNAPSHOT", - "7.1.0", - "7.2.0-SNAPSHOT", - "7.2.0", - "7.3.0-SNAPSHOT", - "7.3.0", - "7.4.0-SNAPSHOT", - "8.0.0-SNAPSHOT", - "9.0.0-SNAPSHOT" - ); - final var expected = new Versioning( - "9.0.0-SNAPSHOT", - "7.3.0", - "20210616221657", - existingVersions - ); - assert expected.equals(artifactMavenMetadata.versioning()); - } -} diff --git a/sonatype/src/test/resources/maven-metadata-example.xml b/sonatype/src/test/resources/maven-metadata-example.xml deleted file mode 100644 index 9d47228b..00000000 --- a/sonatype/src/test/resources/maven-metadata-example.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - org.spongepowered - spongeapi - - 9.0.0-SNAPSHOT - 7.3.0 - - 1.0.0-SNAPSHOT - 1.0 - 1.1-SNAPSHOT - 2.0 - 2.1-SNAPSHOT - 3.0.0 - 3.0.1-SNAPSHOT - 3.0.1-indev - 3.1.0-SNAPSHOT - 3.1.0 - 4.0.0-SNAPSHOT - 4.0.0 - 4.0.1 - 4.0.2 - 4.0.3 - 4.1.0-SNAPSHOT - 4.1.0 - 4.2.0-SNAPSHOT - 5.0.0-SNAPSHOT - 5.0.0 - 5.1.0-SNAPSHOT - 5.1.0 - 5.2.0-SNAPSHOT - 6.0.0-SNAPSHOT - 6.0.0 - 6.1.0-SNAPSHOT - 7.0.0-SNAPSHOT - 7.0.0 - 7.1.0-SNAPSHOT - 7.1.0 - 7.2.0-SNAPSHOT - 7.2.0 - 7.3.0-SNAPSHOT - 7.3.0 - 7.4.0-SNAPSHOT - 8.0.0-SNAPSHOT - 9.0.0-SNAPSHOT - - 20210616221657 - - diff --git a/src/main/java/systemofadownload/AkkaExtension.java b/src/main/java/systemofadownload/AkkaExtension.java deleted file mode 100644 index 25cd72da..00000000 --- a/src/main/java/systemofadownload/AkkaExtension.java +++ /dev/null @@ -1,56 +0,0 @@ -package systemofadownload; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.ActorSystem; -import akka.actor.typed.Scheduler; -import akka.actor.typed.SpawnProtocol; -import akka.actor.typed.javadsl.Adapter; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.sharding.typed.ClusterShardingSettings; -import akka.cluster.sharding.typed.ShardingEnvelope; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import akka.management.cluster.bootstrap.ClusterBootstrap; -import akka.management.javadsl.AkkaManagement; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import io.micronaut.context.annotation.Bean; -import io.micronaut.context.annotation.Factory; -import jakarta.inject.Singleton; - -@Factory -public class AkkaExtension { - - - @Bean - public Scheduler systemScheduler() { - return system().scheduler(); - } - - @Bean - public Config akkaConfig() { - return ConfigFactory.load(); - } - - @Bean(preDestroy = "terminate") - public ActorSystem system() { - Config config = akkaConfig(); - return ActorSystem.create( - Behaviors.setup(ctx -> { - akka.actor.ActorSystem unTypedSystem = Adapter.toClassic(ctx.getSystem()); - AkkaManagement.get(unTypedSystem).start(); - ClusterBootstrap.get(unTypedSystem).start(); - return SpawnProtocol.create(); - }), config.getString("some.cluster.name")); - } - - @Bean - public ClusterSharding clusterSharding() { - return ClusterSharding.get(system()); - } - - @Bean - public ActorRef> someShardRegion() { - return clusterSharding().init(Entity.of(null, null)); - } -} diff --git a/src/main/java/systemofadownload/Application.java b/src/main/java/systemofadownload/Application.java deleted file mode 100644 index 6b627497..00000000 --- a/src/main/java/systemofadownload/Application.java +++ /dev/null @@ -1,18 +0,0 @@ -package systemofadownload; - -import io.micronaut.runtime.Micronaut; -import io.swagger.v3.oas.annotations.*; -import io.swagger.v3.oas.annotations.info.*; - -@OpenAPIDefinition( - info = @Info( - title = "systemofadownload", - version = "0.0" - ) -) -public class Application { - - public static void main(String[] args) { - Micronaut.run(Application.class, args); - } -} \ No newline at end of file diff --git a/src/main/java/systemofadownload/SystemofadownloadController.java b/src/main/java/systemofadownload/SystemofadownloadController.java deleted file mode 100644 index a27608d5..00000000 --- a/src/main/java/systemofadownload/SystemofadownloadController.java +++ /dev/null @@ -1,58 +0,0 @@ -package systemofadownload; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Scheduler; -import akka.actor.typed.javadsl.AskPattern; -import akka.cluster.sharding.typed.ShardingEnvelope; -import io.micronaut.http.annotation.*; -import io.micronaut.security.annotation.Secured; -import io.micronaut.security.rules.SecurityRule; -import io.micronaut.serde.annotation.Serdeable; -import jakarta.inject.Inject; -import reactor.core.publisher.Mono; - -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletionStage; - -@Controller("/systemofadownload") -public class SystemofadownloadController { - record Command() {} - - private final ActorRef> region; - private final Scheduler scheduler; - - @Inject - public SystemofadownloadController( - ActorRef> region, - Scheduler scheduler - ) { - this.region = region; - this.scheduler = scheduler; - - } - - @Get(uri="/", produces="text/plain") - @Secured(SecurityRule.IS_ANONYMOUS) - public String index() { - return "Hello world"; - } - - public static final Duration TIMEOUT = Duration.ofSeconds(10); - - public void someFireForgetMethod(){ - this.region.tell(new ShardingEnvelope<>("foo", new Command())); - } - - record Foo() {} - public Mono someNeedResponseMethod(){ - CompletionStage willBeResponse = AskPattern.ask( - this.region, - replyTo -> new ShardingEnvelope<>("entityId", new Command()), - TIMEOUT, - scheduler - ); - return Mono.fromCompletionStage(willBeResponse); - } -} diff --git a/src/main/java/systemofadownload/UnauthorizedHandler.java b/src/main/java/systemofadownload/UnauthorizedHandler.java deleted file mode 100644 index f7c73c20..00000000 --- a/src/main/java/systemofadownload/UnauthorizedHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package systemofadownload; - -import io.micronaut.context.annotation.Replaces; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.security.authentication.AuthorizationException; -import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler; -import io.micronaut.serde.annotation.Serdeable; -import jakarta.inject.Singleton; - -import java.util.List; - -@Singleton -@Replaces(DefaultAuthorizationExceptionHandler.class) -public class UnauthorizedHandler extends DefaultAuthorizationExceptionHandler { - - @Override - public MutableHttpResponse handle(final HttpRequest request, final AuthorizationException exception) { - return super.handle(request, exception) - .body(new UnauthorizedError(401, List.of("Unauthorized"))); - } - - - @Serdeable - record UnauthorizedError( - int code, List errors - ) { - - } -} diff --git a/src/main/java/systemofadownload/artifacts/ArtifactController.java b/src/main/java/systemofadownload/artifacts/ArtifactController.java deleted file mode 100644 index 001967ae..00000000 --- a/src/main/java/systemofadownload/artifacts/ArtifactController.java +++ /dev/null @@ -1,33 +0,0 @@ -package systemofadownload.artifacts; - -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.PathVariable; -import io.micronaut.http.annotation.Post; -import io.micronaut.http.annotation.QueryValue; -import systemofadownload.artifacts.api.query.ArtifactRegistration; -import systemofadownload.artifacts.api.query.GetArtifactsResponse; - -import java.util.concurrent.Flow; - -@Controller("/groups/{groupID}/artifacts") -public class ArtifactController { - - - @Post("/") - public HttpResponse createArtifact( - @PathVariable String groupID, - @Body ArtifactRegistration.RegisterArtifact registration - ) { - return null; - } - - @Get("/{artifactID}") - public Flow.Publisher> getArtifact( - @PathVariable String groupID, - @PathVariable String artifactID) { - return null; - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/Artifact.java b/src/main/java/systemofadownload/artifacts/api/Artifact.java deleted file mode 100644 index 58746d7d..00000000 --- a/src/main/java/systemofadownload/artifacts/api/Artifact.java +++ /dev/null @@ -1,46 +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 systemofadownload.artifacts.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - -import java.net.URI; -import java.util.Optional; - -@JsonSerialize -public final record Artifact( - @JsonProperty Optional classifier, - @JsonProperty URI downloadUrl, - @JsonProperty String md5, - @JsonProperty String sha1, - @JsonProperty String extension -) { - @JsonCreator - public Artifact { - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java b/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java deleted file mode 100644 index 718b7f57..00000000 --- a/src/main/java/systemofadownload/artifacts/api/ArtifactCollection.java +++ /dev/null @@ -1,43 +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 systemofadownload.artifacts.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.util.List; - -@JsonDeserialize -public final record ArtifactCollection( - @JsonProperty("assets") List components, - @JsonProperty("coordinates") MavenCoordinates coordinates -) { - - @JsonCreator - public ArtifactCollection { - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java b/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java deleted file mode 100644 index 1cd351fa..00000000 --- a/src/main/java/systemofadownload/artifacts/api/ArtifactCoordinates.java +++ /dev/null @@ -1,66 +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 systemofadownload.artifacts.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.util.StringJoiner; - -@JsonDeserialize -public record ArtifactCoordinates( - @JsonProperty(required = true) String groupId, - @JsonProperty(required = true) String artifactId -) { - @JsonCreator - public ArtifactCoordinates { - } - - public MavenCoordinates version(String version) { - return MavenCoordinates.parse( - new StringJoiner(":").add(this.groupId()).add(this.artifactId()).add(version).toString()); - } - - public String asMavenString() { - return this.groupId() + ":" + this.artifactId(); - } - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String groupId() { - return groupId; - } - - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - public String artifactId() { - return artifactId; - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/ArtifactService.java b/src/main/java/systemofadownload/artifacts/api/ArtifactService.java deleted file mode 100644 index 3f7773c4..00000000 --- a/src/main/java/systemofadownload/artifacts/api/ArtifactService.java +++ /dev/null @@ -1,69 +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 systemofadownload.artifacts.api; - -import akka.NotUsed; -import systemofadownload.artifacts.api.event.ArtifactUpdate; -import systemofadownload.artifacts.api.event.GroupUpdate; -import systemofadownload.artifacts.api.query.ArtifactDetails; -import systemofadownload.artifacts.api.query.ArtifactRegistration; -import systemofadownload.artifacts.api.query.GetArtifactsResponse; -import systemofadownload.artifacts.api.query.GroupRegistration; -import systemofadownload.artifacts.api.query.GroupResponse; -import systemofadownload.artifacts.api.query.GroupsResponse; - -public interface ArtifactService { - - ServiceCall, ArtifactDetails.Response> updateDetails(String groupId, String artifactId); - - ServiceCall getGroup(String groupId); - - ServiceCall getGroups(); - - Topic groupTopic(); - - Topic artifactUpdate(); - - @Override - default Descriptor descriptor() { - return Service.named("artifacts") - .withCalls( - Service.restCall(Method.GET, "/artifacts/groups/:groupId", this::getGroup), - Service.restCall(Method.GET, "/artifacts/groups", this::getGroups), - Service.restCall(Method.POST, "/artifacts/groups", this::registerGroup), - Service.restCall(Method.GET, "/artifacts/groups/:groupId/artifacts", this::getArtifacts), - Service.restCall(Method.POST, "/artifacts/groups/:groupId/artifacts", this::registerArtifacts), - Service.restCall(Method.PATCH, "/artifacts/groups/:groupId/artifacts/:artifactId/update", this::updateDetails) - ) - .withTopics( - Service.topic("group-activity", this::groupTopic) - .withProperty(KafkaProperties.partitionKeyStrategy(), GroupUpdate::groupId), - Service.topic("artifact-details-update", this::artifactUpdate) - .withProperty(KafkaProperties.partitionKeyStrategy(), ArtifactUpdate::partitionKey) - ) - .withAutoAcl(true); - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/Group.java b/src/main/java/systemofadownload/artifacts/api/Group.java deleted file mode 100644 index d42e7ee5..00000000 --- a/src/main/java/systemofadownload/artifacts/api/Group.java +++ /dev/null @@ -1,42 +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 systemofadownload.artifacts.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -@JsonDeserialize -public record Group( - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String website -) { - - @JsonCreator - public Group { - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java b/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java deleted file mode 100644 index 18ee831b..00000000 --- a/src/main/java/systemofadownload/artifacts/api/MavenCoordinates.java +++ /dev/null @@ -1,192 +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 systemofadownload.artifacts.api; - -import com.fasterxml.jackson.annotation.JsonCreator; -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; -import java.util.regex.Pattern; - -@JsonDeserialize -public final class MavenCoordinates implements Comparable { - - private static final Pattern MAVEN_REGEX = Pattern.compile("^[-\\w.]+$"); - - /** - * The group id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String groupId; - /** - * The artifact id of an artifact, as defined by the Apache Maven documentation. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String artifactId; - /** - * The version of an artifact, as defined by the Apache Maven documentation. This is - * traditionally specified as a Maven repository searchable version string, such as - * {@code 1.0.0-SNAPSHOT}. - * See Maven Coordinates. - */ - @JsonProperty(required = true) - public final String version; - - @JsonIgnore - public final VersionType versionType; - - @JsonIgnore - private final ComparableVersion mavenVersion; - - /** - * Parses a set of maven formatted coordinates as per - * Apache - * Maven's documentation. - * - * @param coordinates The coordinates delimited by `:` - * @return A parsed set of MavenCoordinates - */ - public static MavenCoordinates parse(final String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - return new MavenCoordinates(groupId, artifactId, version); - } - - public MavenCoordinates(String coordinates) { - final var splitCoordinates = coordinates.split(":"); - if (splitCoordinates.length < 3) { - throw new IllegalArgumentException( - "Coordinates are not formatted or delimited by the `:` character or contains fewer than the required size"); - } - final var groupId = splitCoordinates[0]; - if (!MAVEN_REGEX.asMatchPredicate().test(groupId)) { - throw new IllegalArgumentException("GroupId does not conform to regex rules for a maven group id"); - } - final var artifactId = splitCoordinates[1]; - if (!MAVEN_REGEX.asMatchPredicate().test(artifactId)) { - throw new IllegalArgumentException("ArtifactId does not conform to regex rules for a maven artifact id"); - } - final var version = splitCoordinates[2]; - - VersionType.fromVersion(version); // validates the version is going to be valid somewhat - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonCreator - public MavenCoordinates( - @JsonProperty("groupId") final String groupId, - @JsonProperty("artifactId") final String artifactId, - @JsonProperty("version") final String version - ) { - this.groupId = groupId; - this.artifactId = artifactId; - this.version = version; - this.versionType = VersionType.fromVersion(version); - this.mavenVersion = new ComparableVersion(version); - } - - @JsonIgnore - public String asStandardCoordinates() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.versionType.asStandardVersionString(this.version)) - .toString(); - } - - @JsonIgnore - public boolean isSnapshot() { - return this.versionType.isSnapshot(); - } - - @JsonIgnore - public ArtifactCoordinates asArtifactCoordinates() { - return new ArtifactCoordinates(this.groupId, this.artifactId); - } - - @Override - public String toString() { - return new StringJoiner(":") - .add(this.groupId) - .add(this.artifactId) - .add(this.version) - .toString(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || this.getClass() != o.getClass()) { - return false; - } - final MavenCoordinates that = (MavenCoordinates) o; - return Objects.equals(this.groupId, that.groupId) && Objects.equals( - this.artifactId, that.artifactId) && Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.groupId, this.artifactId, this.version); - } - - @Override - public int compareTo(final MavenCoordinates o) { - final var group = this.groupId.compareTo(o.groupId); - if (group != 0) { - return group; - } - final var artifact = this.artifactId.compareTo(o.artifactId); - if (artifact != 0) { - return artifact; - } - return this.mavenVersion.compareTo(o.mavenVersion); - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/VersionType.java b/src/main/java/systemofadownload/artifacts/api/VersionType.java deleted file mode 100644 index fc9c8a5b..00000000 --- a/src/main/java/systemofadownload/artifacts/api/VersionType.java +++ /dev/null @@ -1,115 +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 systemofadownload.artifacts.api; - -import java.util.StringJoiner; -import java.util.regex.Pattern; - -/** - * In conjunction with {@link MavenCoordinates}, can be used to determine the - * version type of the coordinates, and whether - */ -public enum VersionType { - /** - * A timestamp based file snapshot, such as {@code 1.0.0-20210118.163210-1} - * to where it can be interpreted that the {@link #SNAPSHOT snapshot} version - * would be {@code 1.0.0-SNAPSHOT} that happened to build at date time - * {@code January 18th, 2021 at 16h32m10s} and it's the first build. - */ - TIMESTAMP_SNAPSHOT { - @Override - public boolean isSnapshot() { - return true; - } - - @Override - public String asStandardVersionString(final String version) { - final var split = version.split("-"); - final var stringJoiner = new StringJoiner("-"); - for (int i = 0; i < split.length - 2; i++) { - stringJoiner.add(split[i]); - } - - return stringJoiner.add(SNAPSHOT_VERSION).toString(); - } - }, - - /** - * A standard generic snapshot relative version of a release, such as {@code 1.0.0-SNAPSHOT}. - */ - SNAPSHOT { - @Override - public boolean isSnapshot() { - return true; - } - }, - - /** - * A standard release version not abiding by any snapshot guidelines, considered - * final and singular, such as {@code 1.0.0} - */ - RELEASE; - - /* - Simple SNAPSHOT placeholder - */ - private static final String SNAPSHOT_VERSION = "SNAPSHOT"; - - /* - Verifies the pattern that the snapshot version is date.time-build formatted, - enables the pattern match for a timestamped snapshot - */ - private static final Pattern VERSION_FILE_PATTERN = Pattern.compile("^(.*)-(\\d{8}.\\d{6})-(\\d+)$"); - - private static final Pattern TIMESTAMP_TO_REPLACE = Pattern.compile("(\\d{8}.\\d{6})-(\\d+)$"); - - public static VersionType fromVersion(final String version) { - if (version == null || version.isEmpty()) { - throw new IllegalArgumentException("Version cannot be empty"); - } - // Simple check to find out if the version ends with SNAPSHOT. - if (version.regionMatches( - true, - version.length() - SNAPSHOT_VERSION.length(), - SNAPSHOT_VERSION, - 0, - SNAPSHOT_VERSION.length() - )) { - return SNAPSHOT; - } - if (VERSION_FILE_PATTERN.matcher(version).matches()) { - return TIMESTAMP_SNAPSHOT; - } - return RELEASE; - } - - public boolean isSnapshot() { - return false; - } - - public String asStandardVersionString(final String version) { - return version; - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java b/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java deleted file mode 100644 index 7a88ccb6..00000000 --- a/src/main/java/systemofadownload/artifacts/api/event/ArtifactUpdate.java +++ /dev/null @@ -1,110 +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 systemofadownload.artifacts.api.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import systemofadownload.artifacts.api.ArtifactCoordinates; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(ArtifactUpdate.ArtifactRegistered.class), - @JsonSubTypes.Type(ArtifactUpdate.GitRepositoryAssociated.class), - @JsonSubTypes.Type(ArtifactUpdate.WebsiteUpdated.class), - @JsonSubTypes.Type(ArtifactUpdate.IssuesUpdated.class), - @JsonSubTypes.Type(ArtifactUpdate.DisplayNameUpdated.class), -}) -public interface ArtifactUpdate extends Jsonable { - - ArtifactCoordinates coordinates(); - - default String partitionKey() { - return this.coordinates().asMavenString(); - } - - @JsonTypeName("registered") - @JsonDeserialize - final record ArtifactRegistered( - ArtifactCoordinates coordinates - ) implements ArtifactUpdate { - - @JsonCreator - public ArtifactRegistered { - } - } - - @JsonTypeName("git-repository") - @JsonDeserialize - final record GitRepositoryAssociated( - ArtifactCoordinates coordinates, - String repository - ) implements ArtifactUpdate { - - @JsonCreator - public GitRepositoryAssociated { - } - } - - @JsonTypeName("website") - @JsonDeserialize - final record WebsiteUpdated( - ArtifactCoordinates coordinates, - String url - ) implements ArtifactUpdate { - - @JsonCreator - public WebsiteUpdated { - } - } - - @JsonTypeName("issues") - @JsonDeserialize - final record IssuesUpdated( - ArtifactCoordinates coordinates, - String url - ) implements ArtifactUpdate { - - @JsonCreator - public IssuesUpdated { - } - } - - @JsonTypeName("displayName") - @JsonDeserialize - final record DisplayNameUpdated( - ArtifactCoordinates coordinates, - String displayName - ) implements ArtifactUpdate { - - @JsonCreator - public DisplayNameUpdated { - } - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java b/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java deleted file mode 100644 index 06267ebd..00000000 --- a/src/main/java/systemofadownload/artifacts/api/event/GroupUpdate.java +++ /dev/null @@ -1,74 +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 systemofadownload.artifacts.api.event; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import systemofadownload.artifacts.api.ArtifactCoordinates; - -import java.io.Serial; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(GroupUpdate.GroupRegistered.class), - @JsonSubTypes.Type(GroupUpdate.ArtifactRegistered.class), -}) -public interface GroupUpdate extends Jsonable { - - String groupId(); - - @JsonTypeName("group-registered") - @JsonDeserialize - record GroupRegistered(String groupId, String name, String website) - implements GroupUpdate { - - @JsonCreator - public GroupRegistered { - } - - } - - @JsonTypeName("artifact-registered") - @JsonDeserialize - final record ArtifactRegistered(ArtifactCoordinates coordinates) implements GroupUpdate { - - @Serial private static final long serialVersionUID = 6319289932327553919L; - - @JsonCreator - public ArtifactRegistered { - } - - - @Override - public String groupId() { - return this.coordinates.groupId(); - } - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java b/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java deleted file mode 100644 index 92aa234f..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/ArtifactDetails.java +++ /dev/null @@ -1,129 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.javadsl.api.transport.BadRequest; -import io.vavr.control.Either; -import io.vavr.control.Try; - -import java.net.URL; - -public final class ArtifactDetails { - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Update.Website.class, - name = "website"), - @JsonSubTypes.Type(value = Update.DisplayName.class, - name = "displayName"), - @JsonSubTypes.Type(value = Update.Issues.class, - name = "issues"), - @JsonSubTypes.Type(value = Update.GitRepository.class, - name = "gitRepository"), - }) - @JsonDeserialize - public sealed interface Update { - - Either validate(); - - record Website( - @JsonProperty(required = true) String website - ) implements Update { - - @JsonCreator - public Website { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.website())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.website()))); - } - } - - record DisplayName( - @JsonProperty(required = true) String display - ) implements Update { - - @JsonCreator - public DisplayName { - } - - @Override - public Either validate() { - return Either.right(this.display.trim()); - } - } - - record Issues( - @JsonProperty(required = true) String issues - ) implements Update { - @JsonCreator - public Issues { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.issues())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.issues()))); - } - } - - record GitRepository( - @JsonProperty(required = true) String gitRepo - ) implements Update { - - @JsonCreator - public GitRepository { - } - - @Override - public Either validate() { - return Try.of(() -> new URL(this.gitRepo())) - .toEither(() -> new BadRequest(String.format("Malformed url: %s", this.gitRepo()))); - } - } - } - - @JsonSerialize - public record Response( - String name, - String displayName, - String website, - String issues, - String gitRepo - ) { - - } - - -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java b/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java deleted file mode 100644 index b125349d..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/ArtifactRegistration.java +++ /dev/null @@ -1,81 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 systemofadownload.artifacts.api.ArtifactCoordinates; - -public final class ArtifactRegistration { - - @JsonSerialize - public record RegisterArtifact( - @JsonProperty(required = true) String artifactId, - @JsonProperty(required = true) String displayName - ) { - - @JsonCreator - public RegisterArtifact(final String artifactId, final String displayName) { - this.artifactId = artifactId; - this.displayName = displayName; - } - - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, - name = "UnknownGroup"), - @JsonSubTypes.Type(value = Response.ArtifactRegistered.class, - name = "RegisteredArtifact"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, - name = "AlreadyRegistered"), - }) - public sealed interface Response { - - @JsonSerialize - record ArtifactRegistered(@JsonProperty ArtifactCoordinates coordinates) implements Response { - - } - - @JsonSerialize - record ArtifactAlreadyRegistered( - @JsonProperty String artifactName, - @JsonProperty String groupId - ) implements Response { - - } - - @JsonSerialize - record GroupMissing(@JsonProperty("groupId") String s) implements Response { - - } - - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java deleted file mode 100644 index 13c33681..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/GetArtifactsResponse.java +++ /dev/null @@ -1,60 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 java.util.List; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GetArtifactsResponse.GroupMissing.class, name = "UnknownGroup"), - @JsonSubTypes.Type(value = GetArtifactsResponse.ArtifactsAvailable.class, name = "Artifacts"), -}) -public sealed interface GetArtifactsResponse { - - @JsonSerialize - record GroupMissing(@JsonProperty String groupRequested) implements GetArtifactsResponse { - - @JsonCreator - public GroupMissing { - } - - } - - @JsonSerialize - record ArtifactsAvailable(@JsonProperty List artifactIds) - implements GetArtifactsResponse { - - @JsonCreator - public ArtifactsAvailable { - } - - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java b/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java deleted file mode 100644 index 61e38c2d..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/GroupRegistration.java +++ /dev/null @@ -1,55 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import systemofadownload.artifacts.api.Group; - -public final class GroupRegistration { - - @JsonDeserialize - public record RegisterGroupRequest( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) String groupCoordinates, - @JsonProperty(required = true) String website - ) { - - @JsonCreator - public RegisterGroupRequest { } - - } - - public interface Response { - - record GroupAlreadyRegistered(String groupNameRequested) implements Response { - } - - record GroupRegistered(Group group) implements Response { - - } - } -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java deleted file mode 100644 index ca32acef..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/GroupResponse.java +++ /dev/null @@ -1,61 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 com.lightbend.lagom.serialization.Jsonable; -import systemofadownload.artifacts.api.Group; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GroupResponse.Missing.class, name = "MissingGroup"), - @JsonSubTypes.Type(value = GroupResponse.Available.class, name = "Group") -}) -public sealed interface GroupResponse extends Jsonable { - - @JsonSerialize - record Missing(@JsonProperty String groupId) implements GroupResponse { - @JsonCreator - public Missing(final String groupId) { - this.groupId = groupId; - } - - } - - @JsonSerialize - record Available(@JsonProperty Group group) implements GroupResponse { - - @JsonCreator - public Available(final Group group) { - this.group = group; - } - - } - -} diff --git a/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java b/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java deleted file mode 100644 index 5a0dd0ce..00000000 --- a/src/main/java/systemofadownload/artifacts/api/query/GroupsResponse.java +++ /dev/null @@ -1,48 +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 systemofadownload.artifacts.api.query; - -import com.fasterxml.jackson.annotation.JsonCreator; -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 io.vavr.collection.List; -import systemofadownload.artifacts.api.Group; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GroupsResponse.Available.class, name = "Groups") -}) -public interface GroupsResponse { - - @JsonSerialize - record Available(@JsonProperty List groups) - implements GroupsResponse { - @JsonCreator - public Available { - } - } -} diff --git a/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java b/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java deleted file mode 100644 index 0e4b9992..00000000 --- a/src/main/java/systemofadownload/artifacts/query/ArtifactQueryController.java +++ /dev/null @@ -1,23 +0,0 @@ -package systemofadownload.artifacts.query; - -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.PathVariable; -import io.micronaut.http.annotation.Post; -import systemofadownload.artifacts.api.query.ArtifactRegistration; -import systemofadownload.artifacts.api.query.GetArtifactsResponse; - -import java.util.concurrent.Flow; - -@Controller("/groups/{groupID}/artifacts") -public class ArtifactQueryController { - - - @Get("/") - public Flow.Publisher> getArtifacts( - @PathVariable String groupID - ) { - return null; - } -} diff --git a/src/main/java/systemofadownload/groups/GroupController.java b/src/main/java/systemofadownload/groups/GroupController.java deleted file mode 100644 index ddcbadd2..00000000 --- a/src/main/java/systemofadownload/groups/GroupController.java +++ /dev/null @@ -1,41 +0,0 @@ -package systemofadownload.groups; - -import akka.actor.typed.ActorSystem; -import akka.actor.typed.SpawnProtocol; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.stream.javadsl.AsPublisher; -import akka.stream.javadsl.JavaFlowSupport; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorFlow; -import akka.stream.typed.javadsl.ActorSink; -import io.micronaut.context.annotation.Bean; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.annotation.Body; -import io.micronaut.http.annotation.Controller; -import io.micronaut.http.annotation.Get; -import io.micronaut.http.annotation.PathVariable; -import io.micronaut.http.annotation.Post; -import jakarta.inject.Inject; -import systemofadownload.artifacts.api.query.GetArtifactsResponse; -import systemofadownload.artifacts.api.query.GroupRegistration; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Flow; - -@Controller("/groups") -public class GroupController { - - @Inject - private ActorSystem system; - @Inject - private ClusterSharding sharding; - - @Post("/") - public HttpResponse registerGroup( - @Body GroupRegistration.RegisterGroupRequest req - ) { - return null; - } - -} diff --git a/src/main/java/systemofadownload/groups/query/GroupsQueryController.java b/src/main/java/systemofadownload/groups/query/GroupsQueryController.java deleted file mode 100644 index 3e40b5ab..00000000 --- a/src/main/java/systemofadownload/groups/query/GroupsQueryController.java +++ /dev/null @@ -1,32 +0,0 @@ -package systemofadownload.groups.query; - -import akka.actor.typed.ActorSystem; -import akka.stream.javadsl.AsPublisher; -import akka.stream.javadsl.JavaFlowSupport; -import akka.stream.javadsl.Source; -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 systemofadownload.artifacts.api.query.GetArtifactsResponse; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.Flow; - -@Controller("/groups") -public class GroupsQueryController { - - @Inject - private ActorSystem system; - - @Get(uri = "/{groupID}") - public Flow.Publisher> g(@PathVariable String groupID) { - return Source.from(Arrays.asList("", "b")) - .via(akka.stream.javadsl.Flow.>fromFunction( - s -> HttpResponse.ok(new GetArtifactsResponse.ArtifactsAvailable(List.of(s))) - )) - .runWith(JavaFlowSupport.Sink.asPublisher(AsPublisher.WITH_FANOUT), this.system); - } -} diff --git a/src/main/resources/application.toml b/src/main/resources/application.toml deleted file mode 100644 index 4c3e2b17..00000000 --- a/src/main/resources/application.toml +++ /dev/null @@ -1,20 +0,0 @@ -micronaut.application.name = 'systemofadownload' -liquibase.datasources.default.change-log = 'classpath:db/liquibase-changelog.xml' -jpa.default.reactive = false -netty.default.allocator.max-order = 3 - -[r2dbc.datasources.default] -url = 'r2dbc:postgresql://localhost:5432/postgres' -username = 'postgres' -password = '' -dialect = 'POSTGRES' - -[micronaut.router.static-resources.swagger] -paths = 'classpath:META-INF/swagger' -mapping = '/swagger/**' - -[micronaut.router.static-resources.swagger-ui] -paths = 'classpath:META-INF/swagger/views/swagger-ui' -mapping = '/swagger-ui/**' - -micronaut.security.enabled=false diff --git a/src/main/resources/db/changelog/01-schema.xml b/src/main/resources/db/changelog/01-schema.xml deleted file mode 100644 index 7e9ab55b..00000000 --- a/src/main/resources/db/changelog/01-schema.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main/resources/db/liquibase-changelog.xml b/src/main/resources/db/liquibase-changelog.xml deleted file mode 100644 index 468b33ab..00000000 --- a/src/main/resources/db/liquibase-changelog.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 6010eb52..00000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - true - - - %cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n - - - - - - - diff --git a/src/test/java/systemofadownload/SystemofadownloadTest.java b/src/test/java/systemofadownload/SystemofadownloadTest.java deleted file mode 100644 index 15fe3114..00000000 --- a/src/test/java/systemofadownload/SystemofadownloadTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package systemofadownload; - -import io.micronaut.runtime.EmbeddedApplication; -import io.micronaut.test.extensions.junit5.annotation.MicronautTest; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Assertions; - -import jakarta.inject.Inject; - -@MicronautTest(transactional = false) -class SystemofadownloadTest { - - @Inject - EmbeddedApplication application; - - @Test - void testItWorks() { - Assertions.assertTrue(application.isRunning()); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SonatypeSynchronizer.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SonatypeSynchronizer.java deleted file mode 100644 index 41bd9fec..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SonatypeSynchronizer.java +++ /dev/null @@ -1,95 +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.synchronizer; - -import akka.actor.typed.Behavior; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import akka.cluster.typed.ClusterSingleton; -import akka.cluster.typed.SingletonActor; -import akka.persistence.typed.PersistenceId; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.synchronizer.actor.CommitRegistrar; -import org.spongepowered.synchronizer.assetsync.VersionConsumer; -import org.spongepowered.synchronizer.gitmanaged.ArtifactSubscriber; -import org.spongepowered.synchronizer.gitmanaged.CommitConsumer; -import org.spongepowered.synchronizer.gitmanaged.ScheduledCommitResolver; -import org.spongepowered.synchronizer.gitmanaged.domain.GitManagedArtifact; -import org.spongepowered.synchronizer.resync.ResyncManager; -import org.spongepowered.synchronizer.resync.domain.ArtifactSynchronizerAggregate; -import org.spongepowered.synchronizer.versionsync.ArtifactConsumer; - -public final class SonatypeSynchronizer { - - public interface Command { - } - - public static Behavior create( - final ArtifactService artifactService, - final VersionsService versionsService, - final ClusterSharding clusterSharding, - final ObjectMapper mapper - ) { - return Behaviors.setup(context -> { - // don't do this, eventually we can swap this out from service layers - context.spawnAnonymous(Behaviors.supervise(CommitRegistrar.register(versionsService)) - .onFailure(SupervisorStrategy.restart())); - CommitConsumer.setupSubscribers(versionsService, context); - ArtifactSubscriber.setup(artifactService, context); - ScheduledCommitResolver.setup(artifactService, context); - - final var settings = SynchronizationExtension.SettingsProvider.get(context.getSystem()); - clusterSharding - .init( - Entity.of( - ArtifactSynchronizerAggregate.ENTITY_TYPE_KEY, - ArtifactSynchronizerAggregate::create - ) - ); - clusterSharding.init(Entity.of(GitManagedArtifact.ENTITY_TYPE_KEY, ctx -> GitManagedArtifact.create(PersistenceId.of(ctx.getEntityTypeKey().name(), ctx.getEntityId()), ctx.getEntityId()))); - - ArtifactConsumer.subscribeToArtifactUpdates( - context, artifactService, versionsService, clusterSharding, settings); - - VersionConsumer.subscribeToVersionedArtifactUpdates(versionsService, mapper, context, settings); - - final var resyncManager = ResyncManager.create(artifactService, settings.versionSync); - final var resyncBehavior = Behaviors.supervise(resyncManager) - .onFailure( - SupervisorStrategy.restart()); - final var actor = SingletonActor.of(resyncBehavior, "artifact-sync"); - ClusterSingleton.get(context.getSystem()).init(actor); - - // Scheduled full resynchronization with maven and therefor sonatype - return Behaviors.receive(SonatypeSynchronizer.Command.class) - .build(); - }); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizationExtension.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizationExtension.java deleted file mode 100644 index d22e5ee6..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizationExtension.java +++ /dev/null @@ -1,47 +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.synchronizer; - -import akka.actor.AbstractExtensionId; -import akka.actor.ExtendedActorSystem; -import akka.actor.Extension; -import akka.actor.ExtensionId; -import akka.actor.ExtensionIdProvider; - -public class SynchronizationExtension extends AbstractExtensionId implements ExtensionIdProvider { - - public static final SynchronizationExtension SettingsProvider = new SynchronizationExtension(); - - @Override - public SynchronizerSettings createExtension(final ExtendedActorSystem system) { - return new SynchronizerSettings(system.settings().config().getConfig("systemofadownload.synchronizer")); - } - - - @Override - public ExtensionId lookup() { - return SettingsProvider; - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerModule.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerModule.java deleted file mode 100644 index f840b72e..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerModule.java +++ /dev/null @@ -1,95 +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.synchronizer; - -import akka.actor.ActorSystem; -import akka.actor.typed.ActorRef; -import akka.actor.typed.javadsl.Adapter; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.inject.AbstractModule; -import com.google.inject.Inject; -import com.google.inject.Provider; -import com.google.inject.Provides; -import com.google.inject.TypeLiteral; -import com.lightbend.lagom.javadsl.api.ServiceInfo; -import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; -import org.pac4j.core.config.Config; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.auth.SOADAuth; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import play.Environment; -import play.api.libs.concurrent.AkkaGuiceSupport; - -public class SynchronizerModule extends AbstractModule implements ServiceGuiceSupport, AkkaGuiceSupport { - - private final AuthUtils auth; - - @Inject - public SynchronizerModule(final Environment environment, final com.typesafe.config.Config config) { - this.auth = AuthUtils.configure(config); - } - - @Override - protected void configure() { - this.bindClient(VersionsService.class); - this.bindClient(ArtifactService.class); - this.bindServiceInfo(ServiceInfo.of("Sonatype-Synchronizer")); - this.bind(new TypeLiteral>() { - }) - .toProvider(SynchronizerProvider.class) - .asEagerSingleton(); - } - - @Provides - @SOADAuth - protected Config configProvider() { - return this.auth.config(); - } - - public record SynchronizerProvider( - ArtifactService artifactService, - VersionsService versionsService, - ClusterSharding clusterSharding, - ObjectMapper mapper, - ActorSystem system - ) implements Provider> { - - @Inject - public SynchronizerProvider { - } - - @Override - public ActorRef get() { - return Adapter.spawn(this.system, SonatypeSynchronizer.create( - this.artifactService, - this.versionsService, - this.clusterSharding, - this.mapper - ), "Synchronizer"); - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerSettings.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerSettings.java deleted file mode 100644 index 660363f0..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/SynchronizerSettings.java +++ /dev/null @@ -1,97 +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.synchronizer; - -import akka.actor.Extension; -import akka.actor.typed.BackoffSupervisorStrategy; -import akka.actor.typed.SupervisorStrategy; -import com.typesafe.config.Config; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -public class SynchronizerSettings implements Extension { - - public final ReactiveSync reactiveSync; - public final Asset asset; - public final VersionSync versionSync; - - public static final class Asset { - public final int poolSize; - public final int parallelism; - public final Duration initialBackoff; - public final Duration maximumBackoff; - public final double backoffFactor; - public final Duration timeout; - - public final BackoffSupervisorStrategy backoff; - - Asset(Config config) { - this.poolSize = config.getInt("pool-size"); - this.initialBackoff = Duration.ofSeconds(config.getDuration("initial-backoff", TimeUnit.SECONDS)); - this.maximumBackoff = Duration.ofSeconds(config.getDuration("maximum-backoff", TimeUnit.SECONDS)); - this.backoffFactor = config.getDouble("backoff-factor"); - this.backoff = SupervisorStrategy.restartWithBackoff( - this.initialBackoff, this.maximumBackoff, this.backoffFactor); - this.parallelism = config.getInt("parallelism"); - this.timeout = Duration.ofMinutes(config.getDuration("time-out", TimeUnit.MINUTES)); - } - } - - public static final class VersionSync { - public final int versionSyncPoolSize; - public final Duration interval; - public final Duration startupDelay; - - VersionSync(Config config) { - this.versionSyncPoolSize = config.getInt("pool-size"); - this.interval = Duration.ofSeconds(config.getDuration("interval", TimeUnit.SECONDS)); - this.startupDelay = Duration.ofSeconds(config.getDuration("delay", TimeUnit.SECONDS)); - } - } - - public static final class ReactiveSync { - public final int poolSize; - public final int parallelism; - public final Duration timeOut; - - ReactiveSync(Config config) { - this.poolSize = config.getInt("pool-size"); - this.parallelism = config.getInt("parallelism"); - this.timeOut = Duration.ofMinutes(config.getDuration("time-out", TimeUnit.MINUTES)); - } - } - - public SynchronizerSettings(Config config) { - final var assetConfig = config.getConfig("asset"); - this.asset = new Asset(assetConfig); - - final var versionSync = config.getConfig("version-sync"); - this.versionSync = new VersionSync(versionSync); - - final var reactiveSync = config.getConfig("reactive-sync"); - this.reactiveSync = new ReactiveSync(reactiveSync); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncExtension.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncExtension.java deleted file mode 100644 index 3e4f69c5..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncExtension.java +++ /dev/null @@ -1,84 +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.synchronizer.actor; - -import akka.actor.AbstractExtensionId; -import akka.actor.ExtendedActorSystem; -import akka.actor.Extension; -import akka.actor.ExtensionId; -import akka.actor.ExtensionIdProvider; -import com.typesafe.config.Config; - -import java.time.Duration; -import java.util.StringJoiner; -import java.util.concurrent.TimeUnit; - -public class ArtifactSyncExtension extends AbstractExtensionId - implements ExtensionIdProvider { - - public static final ArtifactSyncExtension SettingsProvider = new ArtifactSyncExtension(); - - @Override - public Settings createExtension(final ExtendedActorSystem system) { - return new Settings( - system.settings().config().getConfig("systemofadownload.synchronizer.worker.version-registration")); - } - - - @Override - public ExtensionId lookup() { - return SettingsProvider; - } - - public static final class Settings implements Extension { - - public final int poolSize; - public final int versionFanoutParallelism; - public final int parallelism; - public final Duration timeOut; - public final Duration individualTimeOut; - - public Settings(Config config) { - this.poolSize = config.getInt("pool-size"); - this.versionFanoutParallelism = config.getInt("fan-out-parallelism"); - this.parallelism = config.getInt("parallelism"); - this.timeOut = Duration.ofSeconds(config.getDuration("time-out", TimeUnit.SECONDS)); - this.individualTimeOut = Duration.ofSeconds(config.getDuration("registration-time-out", TimeUnit.SECONDS)); - - } - - @Override - public String toString() { - return new StringJoiner( - ", ", Settings.class.getSimpleName() + "[", "]") - .add("poolSize=" + poolSize) - .add("versionFanoutParallelism=" + versionFanoutParallelism) - .add("parallelism=" + parallelism) - .add("timeOut=" + timeOut) - .add("individualTimeOut=" + individualTimeOut) - .toString(); - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncWorker.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncWorker.java deleted file mode 100644 index adfc6f72..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/ArtifactSyncWorker.java +++ /dev/null @@ -1,134 +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.synchronizer.actor; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.synchronizer.resync.domain.ArtifactSynchronizerAggregate; -import org.spongepowered.synchronizer.versionsync.ArtifactVersionSyncEntity; -import org.spongepowered.synchronizer.versionsync.SyncRegistration; - -import java.time.Duration; - -public final class ArtifactSyncWorker { - - public interface Command { - } - - /** - * Starts the request that the specific artifact described by the - * {@link ArtifactCoordinates coordinates} are synchronized, versioned, - * and other side effects. A {@link Done} response is always given, - * regardless of outcome, since the sync performs multiple jobs in the - * background. - */ - public record PerformResync(ArtifactCoordinates coordinates, ActorRef replyTo) - implements Command { - } - - /** - * For use with just getting a {@link Done} reply back, some messages can be ignored. - */ - public record Ignored(ActorRef replyTo) implements Command { - } - - private record Failed(ArtifactCoordinates coordinates, ActorRef replyTo) implements Command { - } - - /** - * A post-reqeusted result signifying that the initial request for an - * artifact's versions to sync has been completed. - */ - private record WrappedResult(ActorRef replyTo) implements Command { - } - - public static Behavior create( - final ClusterSharding clusterSharding - ) { - return Behaviors.setup(ctx -> { - final ArtifactSyncExtension.Settings settings = ArtifactSyncExtension.SettingsProvider.get(ctx.getSystem()); - return awaiting(clusterSharding, settings); - }); - } - - private static Behavior awaiting( - final ClusterSharding clusterSharding, - final ArtifactSyncExtension.Settings settings - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage(PerformResync.class, msg -> { - ctx.getLog().debug("Running Sync"); - final var globalResyncRef = clusterSharding.entityRefFor( - ArtifactSynchronizerAggregate.ENTITY_TYPE_KEY, - msg.coordinates.asMavenString() - ); - final var versionSyncRef = clusterSharding.entityRefFor( - ArtifactVersionSyncEntity.ENTITY_TYPE_KEY, - msg.coordinates.asMavenString() - ); - ctx.pipeToSelf( - globalResyncRef - .>ask( - replyTo -> new org.spongepowered.synchronizer.resync.domain.Command.Resync(msg.coordinates, replyTo), settings.individualTimeOut) - .thenCompose(response -> - versionSyncRef - .ask( - replyTo -> new SyncRegistration.SyncBatch(msg.coordinates, response, replyTo), - Duration.ofMinutes(10) - ) - ), - (ok, exception) -> { - if (exception != null) { - ctx.getLog().error("Failed to resync by maven coordinates, may ask again", exception); - return new Failed(msg.coordinates, msg.replyTo); - } - - return new WrappedResult(msg.replyTo); - } - ); - return Behaviors.same(); - }) - .onMessage(WrappedResult.class, msg -> { - msg.replyTo.tell(Done.done()); - return Behaviors.same(); - }) - .onMessage(Ignored.class, msg -> { - msg.replyTo.tell(Done.done()); - return Behaviors.same(); - }) - .onMessage(Failed.class, msg -> { - msg.replyTo.tell(Done.done()); - return Behaviors.same(); - }) - .build()); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitDetailsRegistrar.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitDetailsRegistrar.java deleted file mode 100644 index 8523c78b..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitDetailsRegistrar.java +++ /dev/null @@ -1,84 +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.synchronizer.actor; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.receptionist.ServiceKey; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; - -public final class CommitDetailsRegistrar { - - public static final ServiceKey SERVICE_KEY = ServiceKey.create(Command.class, "commit-details-registrar"); - - @JsonDeserialize - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(HandleVersionedCommitReport.class), - @JsonSubTypes.Type(CommitNotFound.class), - @JsonSubTypes.Type(CompletedWork.class) - }) - public sealed interface Command extends Jsonable {} - - @JsonTypeName("handle-version-commit") - public record HandleVersionedCommitReport( - URI repo, - VersionedCommit versionedCommit, - MavenCoordinates coordinates, - ActorRef replyTo - ) implements Command { - @JsonCreator - public HandleVersionedCommitReport { - } - } - - @JsonTypeName("commit-not-found") - public record CommitNotFound( - URI repo, - String commitId, - MavenCoordinates coordinates, - ActorRef replyTo - ) implements Command { - @JsonCreator - public CommitNotFound { - } - } - - @JsonTypeName("completed-work") - record CompletedWork(ActorRef replyTo) implements Command { - @JsonCreator - public CompletedWork { - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitRegistrar.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitRegistrar.java deleted file mode 100644 index 9fb61f92..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/actor/CommitRegistrar.java +++ /dev/null @@ -1,94 +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.synchronizer.actor; - -import akka.Done; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.receptionist.Receptionist; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.CommitRegistration; -import org.spongepowered.synchronizer.gitmanaged.domain.GitCommand; -import org.spongepowered.synchronizer.gitmanaged.domain.GitManagedArtifact; - -import java.time.Duration; - -public class CommitRegistrar { - - public static Behavior register( - VersionsService versionsService - ) { - return Behaviors.setup(ctx -> { - final var registration = Receptionist.register(CommitDetailsRegistrar.SERVICE_KEY, ctx.getSelf()); - ctx.getSystem().receptionist().tell(registration); - final var sharding = ClusterSharding.get(ctx.getSystem()); - final var auth = AuthUtils.configure(ctx.getSystem().settings().config()); - return Behaviors.receive(CommitDetailsRegistrar.Command.class) - .onMessage(CommitDetailsRegistrar.HandleVersionedCommitReport.class, msg -> { - final var future = auth.internalAuth(versionsService.registerCommit( - msg.coordinates().groupId, msg.coordinates().artifactId, msg.coordinates().version - )).invoke(new CommitRegistration.ResolvedCommit( - msg.repo(), - msg.versionedCommit(), - msg.coordinates() - )).toCompletableFuture(); - ctx.pipeToSelf(future, (done, failure) -> { - if (failure != null) { - ctx.getLog().warn("Failed registering git details", failure); - } - return new CommitDetailsRegistrar.CompletedWork(msg.replyTo()); - }); - return Behaviors.same(); - }) - .onMessage(CommitDetailsRegistrar.CommitNotFound.class, msg -> { - final var future = auth.internalAuth(versionsService.registerCommit( - msg.coordinates().groupId, msg.coordinates().artifactId, msg.coordinates().version - )).invoke(new CommitRegistration.FailedCommit(msg.commitId(), msg.repo())) - .thenCompose(done -> sharding.entityRefFor(GitManagedArtifact.ENTITY_TYPE_KEY, - msg.coordinates().asArtifactCoordinates().asMavenString()) - .ask(replyTo -> new GitCommand.MarkVersionAsUnresolveable(replyTo, msg.coordinates(), msg.commitId()), - Duration.ofMinutes(10))) - .toCompletableFuture(); - - ctx.pipeToSelf(future, (done, failure) -> { - if (failure != null) { - ctx.getLog().warn("Failed registering git details", failure); - } - return new CommitDetailsRegistrar.CompletedWork(msg.replyTo()); - }); - return Behaviors.same(); - }) - .onMessage(CommitDetailsRegistrar.CompletedWork.class, msg -> { - msg.replyTo().tell(Done.getInstance()); - return Behaviors.same(); - }) - .build(); - }); - - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/akka/FlowUtil.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/akka/FlowUtil.java deleted file mode 100644 index 879b77d9..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/akka/FlowUtil.java +++ /dev/null @@ -1,122 +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.synchronizer.akka; - -import akka.Done; -import akka.NotUsed; -import akka.japi.Pair; -import akka.stream.FlowShape; -import akka.stream.UniformFanInShape; -import akka.stream.UniformFanOutShape; -import akka.stream.javadsl.Broadcast; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.GraphDSL; -import akka.stream.javadsl.Merge; -import akka.stream.javadsl.Partition; -import io.vavr.Tuple; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import io.vavr.control.Option; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public final class FlowUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger("FlowUtil"); - - @SuppressWarnings("unchecked") - @SafeVarargs - public static Flow broadcast(Flow... flows) { - final var gatheredFlows = Arrays.stream(flows).collect(List.collector()); - final var count = gatheredFlows.size(); - return Flow.fromGraph(GraphDSL.create(builder -> { - final var broadcast = builder.add(Broadcast.create(count)); - final var merge = builder.add(Merge.create(count)); - for (int i = 0; i < count; i++) { - builder.from(broadcast.out(i)) - .via(builder.add(gatheredFlows.get(i))) - .toInlet(merge.in(i)); - } - return FlowShape.apply(broadcast.in(), merge.out()); - })); - } - - @SuppressWarnings("unchecked") - @SafeVarargs - public static Flow splitClassFlows( - Pair, Flow>... pairs - ) { - final List, Flow>> flowPairs = Arrays.stream(pairs) - .collect(List.collector()); - final var count = flowPairs.size(); - - final Map, Integer> classToIndex = HashMap.ofEntries( - IntStream.range(0, count) - .mapToObj(i -> Tuple.of(pairs[i].first(), i)) - .collect(Collectors.toList()) - ); - - final Function decider = (message) -> flowPairs.map(Pair::first) - .filter(clazz -> clazz.isInstance(message)) - .map(classToIndex::get) - .filter(Option::isDefined) - .map(Option::get) - .getOrElse(count); - - final Flow ignored = Flow.fromFunction(message -> { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("ignoring message {}", message); - } - return Done.done(); - }); - return Flow.fromGraph(GraphDSL.create(builder -> { - final UniformFanInShape merge = builder.add(Merge.create(count + 1)); - final UniformFanOutShape fanout = builder - .add(Partition.create(count + 1, decider::apply)); - for (int i = 0; i < count; i++) { - builder.from(fanout.out(i)) - .via(builder.add((Flow) flowPairs.get(i).second().async())) - .toInlet(merge.in(i)); - } - builder.from(fanout.out(count)) - .via(builder.add(ignored)) - .toInlet(merge.in(count)); - return FlowShape.of(fanout.in(), merge.out()); - })); - } - - @SuppressWarnings("unchecked") - public static Flow subClassFlow(Flow subFlow) { - return Flow.create() - .map(t -> (S) t) - .via(subFlow); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/AssetSettingsExtension.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/AssetSettingsExtension.java deleted file mode 100644 index bcae2f5c..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/AssetSettingsExtension.java +++ /dev/null @@ -1,69 +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.synchronizer.assetsync; - -import akka.actor.AbstractExtensionId; -import akka.actor.ExtendedActorSystem; -import akka.actor.Extension; -import akka.actor.ExtensionIdProvider; -import com.typesafe.config.Config; -import io.vavr.collection.List; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -class AssetSettingsExtension extends AbstractExtensionId - implements ExtensionIdProvider { - public static final AssetSettingsExtension SettingsProvider = new AssetSettingsExtension(); - - @Override - public AssetRetrievalSettings createExtension(final ExtendedActorSystem system) { - return new AssetRetrievalSettings( - system.settings().config().getConfig("systemofadownload.synchronizer.worker.assets")); - } - - @Override - public AssetSettingsExtension lookup() { - return SettingsProvider; - } - - public static class AssetRetrievalSettings implements Extension { - public final String repository; - public final Duration timeout; - public final int retryCount; - public final List filesToIndex; - public final int poolSize; - - public AssetRetrievalSettings(Config config) { - this.repository = config.getString("repository"); - this.retryCount = config.getInt("retry"); - final var seconds = config.getDuration("timeout", TimeUnit.SECONDS); - this.timeout = Duration.ofSeconds(seconds); - final var stringList = config.getStringList("files-to-index"); - this.filesToIndex = List.ofAll(stringList); - this.poolSize = config.getInt("pool-size"); - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionConsumer.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionConsumer.java deleted file mode 100644 index b626b098..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionConsumer.java +++ /dev/null @@ -1,74 +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.synchronizer.assetsync; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.stream.javadsl.Flow; -import akka.stream.typed.javadsl.ActorFlow; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.ArtifactUpdate; -import org.spongepowered.synchronizer.SonatypeSynchronizer; -import org.spongepowered.synchronizer.SynchronizerSettings; - -public class VersionConsumer { - public static void subscribeToVersionedArtifactUpdates( - final VersionsService versionsService, final ObjectMapper mapper, - final ActorContext context, - final SynchronizerSettings settings - ) { - // region Synchronize Versioned Assets through Sonatype Search - final var componentPool = Routers.pool( - settings.asset.poolSize, - Behaviors.supervise(VersionedComponentWorker.gatherComponents(versionsService, mapper)) - .onFailure(settings.asset.backoff) - ); - final var componentRef = context.spawn( - componentPool, - "version-component-registration", - DispatcherSelector.defaultDispatcher() - ); - final Flow versionedFlow = ActorFlow.ask( - settings.asset.parallelism, - componentRef, - settings.asset.timeout, - (g, b) -> { - if (!(g instanceof ArtifactUpdate.ArtifactVersionRegistered a)) { - return new VersionedComponentWorker.Ignored(b); - } - return new VersionedComponentWorker.GatherComponentsForArtifact(a.coordinates(), b); - } - ); - versionsService.artifactUpdateTopic() - .subscribe() - .atLeastOnce(versionedFlow); - // endregion - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionedComponentWorker.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionedComponentWorker.java deleted file mode 100644 index fec41c61..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/assetsync/VersionedComponentWorker.java +++ /dev/null @@ -1,427 +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.synchronizer.assetsync; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.pattern.CircuitBreakerOpenException; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.vavr.collection.List; -import io.vavr.control.Try; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.ArtifactCollection; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.sonatype.AssetSearchResponse; -import org.spongepowered.downloads.sonatype.Component; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; - -import java.net.URI; -import java.net.URL; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeoutException; - -public final class VersionedComponentWorker { - public static final String ASSET_SEARCH_ENDPOINT = - """ - /service/rest/v1/search/assets?maven.groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=%s - """.trim(); - public static final String ASSET_SEARCH_WITH_TOKEN = - """ - /service/rest/v1/search/assets?continuationToken=%s&maven.groupId=%s&maven.artifactId=%s&maven.baseVersion=%s&maven.extension=%s - """.trim(); - - public interface Command { - } - - public record GatherComponentsForArtifact( - MavenCoordinates coordinates, - int count, ActorRef replyTo) - implements Command { - - public GatherComponentsForArtifact(MavenCoordinates coordinates, ActorRef replyTo) { - this(coordinates, 0, replyTo); - } - } - - public record Ignored(ActorRef replyTo) implements Command { - - } - - private interface ChildResponse extends Command { - } - - private record ComponentsAvailable( - MavenCoordinates coordinates, - List assets, - ActorRef replyTo - ) implements ChildResponse { - } - - private record FailedAssetRetrieval( - MavenCoordinates coordinates, - int count, - ActorRef replyTo - ) implements ChildResponse { - } - - public static Behavior gatherComponents( - final VersionsService service, - final ObjectMapper mapper - ) { - return Behaviors.setup(ctx -> { - final AssetSettingsExtension.AssetRetrievalSettings config = AssetSettingsExtension.SettingsProvider.get( - ctx.getSystem()); - final var assetDispatcher = DispatcherSelector.fromConfig("asset-retrieval-dispatcher"); - final var gathererPool = Routers.pool(config.poolSize, idleFetcher(config, mapper)); - final var gathererRef = ctx.spawn( - Behaviors.supervise(gathererPool).onFailure(SupervisorStrategy.restart()), - "search-versioned-components", - assetDispatcher - ); - final var auth = AuthUtils.configure(ctx.getSystem().settings().config()); - final var assetRegisters = Routers.pool(config.poolSize, registerAssets(service, auth)); - final var assetRegistersRef = ctx.spawn( - Behaviors.supervise(assetRegisters).onFailure(SupervisorStrategy.restart()), - "versioned-asset-register", - assetDispatcher - ); - return idleGatherer(gathererRef, assetRegistersRef, config); - }); - } - - private static Behavior idleGatherer( - final ActorRef gathererRef, - final ActorRef assetRegistersRef, - final AssetSettingsExtension.AssetRetrievalSettings config - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage(GatherComponentsForArtifact.class, cmd -> { - config.filesToIndex.forEach(fileType -> ctx.ask( - ChildResponse.class, - gathererRef, - config.timeout, - ref -> new StartRequest(cmd.coordinates, fileType, ref, cmd.replyTo), - (response, throwable) -> { - if (throwable != null) { - return new FailedAssetRetrieval(cmd.coordinates, cmd.count + 1, cmd.replyTo); - } - // In case the child returned a failed asset retrieval - if (response instanceof FailedAssetRetrieval f) { - return new FailedAssetRetrieval(f.coordinates, f.count + 1, cmd.replyTo); - } - return response; - } - )); - return Behaviors.same(); - }) - .onMessage(ComponentsAvailable.class, available -> { - // now we've gotta register the components to the version service - available.replyTo.tell(Done.done()); - assetRegistersRef.tell(new AttemptRegistration(available.coordinates, available.assets)); - return Behaviors.same(); - }) - .onMessage(FailedAssetRetrieval.class, failed -> { - // Retry basically - ctx.getLog().error("Failed artifact attempt"); - if (failed.count >= config.retryCount) { - ctx.getLog().error("Aborting attempt to sync artifact assets"); - failed.replyTo.tell(Done.done()); - return Behaviors.same(); - } - ctx.getSelf().tell(new GatherComponentsForArtifact(failed.coordinates, failed.count, failed.replyTo)); - return Behaviors.same(); - }) - .onMessage(Ignored.class, ignored -> { - ignored.replyTo.tell(Done.done()); - return Behaviors.same(); - }) - .build()); - } - - private interface Gather { - } - - private record StartRequest( - MavenCoordinates coordinates, - String fileType, - ActorRef ref, - ActorRef completedReplyTo - ) implements Gather { - } - - private record ContinueRequest( - MavenCoordinates coordinates, - String fileType, - List existing, - String continuationToken, - ActorRef replyTo, - ActorRef completedReplyTo - ) implements Gather { - } - - private record Completed( - MavenCoordinates coordinates, - List existing, - ActorRef replyTo, - ActorRef completedReplyTo - ) implements Gather { - } - - private record Failed( - MavenCoordinates coordinates, - List recovered, - ActorRef replyTo, - ActorRef completedReplyTo - ) implements Gather { - } - - private static Behavior idleFetcher( - final AssetSettingsExtension.AssetRetrievalSettings config, - final ObjectMapper playMapper - ) { - return Behaviors.setup(ctx -> { - final var client = HttpClient.newBuilder().connectTimeout(config.timeout) - .executor(ctx.getExecutionContext()) - .build(); - return Behaviors.receive(Gather.class) - .onMessage(StartRequest.class, req -> { - runRequest(config, playMapper, ctx, client, req); - return working(config, playMapper, req, List.empty()); - }) - .onMessage(ContinueRequest.class, cont -> { - ctx.getLog().warn("Somehow got a continuation while idling"); - return Behaviors.same(); - }) - .onMessage(Completed.class, completed -> { - ctx.getLog().warn("Somehow got completed while idling"); - return Behaviors.same(); - }) - .onMessage(Failed.class, failed -> { - failed.replyTo.tell(new FailedAssetRetrieval(failed.coordinates, 0, failed.completedReplyTo)); - return Behaviors.same(); - }) - .build(); - }); - } - - private static void runRequest( - AssetSettingsExtension.AssetRetrievalSettings config, ObjectMapper playMapper, ActorContext ctx, - HttpClient client, StartRequest req - ) { - final var formatted = String.format( - config.repository + ASSET_SEARCH_ENDPOINT, - req.coordinates.groupId, - req.coordinates.artifactId, - req.coordinates.version, - req.fileType - ); - ctx.pipeToSelf(searchAssets(playMapper, client, formatted), (response, failure) -> { - if (failure != null) { - return new Failed(req.coordinates, List.empty(), req.ref, req.completedReplyTo); - } - return response.continuationToken() - .map( - token -> new ContinueRequest( - req.coordinates, req.fileType, response.items(), token, req.ref, - req.completedReplyTo - )) - .orElseGet( - () -> new Completed(req.coordinates, response.items(), req.ref, req.completedReplyTo)); - }); - } - - private static Behavior working(final AssetSettingsExtension.AssetRetrievalSettings config, - final ObjectMapper playMapper, - final StartRequest working, - final List queue - ) { - return Behaviors.setup(ctx -> { - final var client = HttpClient.newBuilder().connectTimeout(config.timeout) - .executor(ctx.getExecutionContext()) - .build(); - - return Behaviors.receive(Gather.class) - .onMessage(StartRequest.class, req -> working(config, playMapper, working, queue.append(req))) - .onMessage(ContinueRequest.class, cont -> { - final var formatted = String.format( - config.repository + ASSET_SEARCH_WITH_TOKEN, - cont.continuationToken, - cont.coordinates.groupId, - cont.coordinates.artifactId, - cont.coordinates.version, - cont.fileType - ); - ctx.pipeToSelf(searchAssets(playMapper, client, formatted), (response, throwable) -> { - if (throwable != null) { - return new Failed(cont.coordinates, cont.existing, cont.replyTo, cont.completedReplyTo); - } - final var completedAssets = cont.existing.appendAll(response.items()); - return response.continuationToken() - .map( - token -> new ContinueRequest( - cont.coordinates, cont.fileType, completedAssets, token, cont.replyTo, - cont.completedReplyTo - )) - .orElseGet(() -> new Completed(cont.coordinates, completedAssets, cont.replyTo, - cont.completedReplyTo - )); - }); - return Behaviors.same(); - }) - .onMessage(Completed.class, completed -> { - completed.replyTo.tell( - new ComponentsAvailable(completed.coordinates, completed.existing, completed.completedReplyTo)); - if (queue.isEmpty()) { - return idleFetcher(config, playMapper); - } - final var next = queue.head(); - runRequest(config, playMapper, ctx, client, next); - return working(config, playMapper, next, queue.tail()); - }) - .onMessage(Failed.class, failed -> { - runRequest(config, playMapper, ctx, client, working); - return Behaviors.same(); - }) - .build(); - }); - } - - private static CompletableFuture searchAssets( - final ObjectMapper playMapper, - final HttpClient client, - final String assetSearchUrl - ) { - return Try.of(() -> new URL(assetSearchUrl)) - .mapTry(URL::toURI) - .map(url -> HttpRequest.newBuilder(url).GET()) - .toCompletableFuture() - .thenCompose(req -> client.sendAsync(req.build(), HttpResponse.BodyHandlers.ofInputStream())) - .thenApply(HttpResponse::body) - .thenApply(stream -> Try.withResources(() -> stream) - .of(is -> playMapper.readValue(is, AssetSearchResponse.class)) - .get()); - } - - private interface Registrar { - } - - private record AttemptRegistration(MavenCoordinates coordinates, - List assets) implements Registrar { - } - - private interface RegistrationResult extends Registrar { - } - - private record AssetRegistrationCompleted(MavenCoordinates coordinates) implements RegistrationResult { - } - - private record AssetRegistrationFailed(MavenCoordinates coordinates) implements RegistrationResult { - } - - private record WrappedResult(RegistrationResult response) implements Registrar { - } - - private static Behavior registerAssets( - final VersionsService service, - final AuthUtils auth - ) { - return Behaviors.setup(ctx -> - Behaviors.withTimers(timers -> Behaviors.receive(Registrar.class) - .onMessage(AttemptRegistration.class, registration -> { - final var artifacts = registration.assets.map(asset -> - Try.of(() -> URI.create(asset.downloadUrl())) - .map(downloadUri -> Optional.ofNullable(asset.mavenData()) - .map(data -> new Artifact( - Optional.ofNullable(data.classifier()), - downloadUri, - asset.checksum().md5(), - asset.checksum().sha1(), - asset.mavenData().extension() - )) - ) - .filter(Optional::isPresent) - .map(Optional::get) - .toJavaOptional() - ) - .filter(Optional::isPresent) - .map(Optional::get); - final var collection = new ArtifactCollection(artifacts, registration.coordinates); - - ; - final var registrationFuture = auth.internalAuth(service.registerArtifactCollection( - registration.coordinates.groupId, - registration.coordinates.artifactId - )).invoke(new VersionRegistration.Register.Collection(collection)); - - ctx.pipeToSelf(registrationFuture, (response, throwable) -> { - if (throwable != null) { - if (throwable instanceof CompletionException ce) { - throwable = ce.getCause(); - } - if (throwable instanceof CircuitBreakerOpenException cboe) { - ctx.getLog().warn("Rescheduling asset registration for {}", registration.coordinates); - timers.startSingleTimer(registration.coordinates, registration, Duration.ofMillis(cboe.remainingDuration().toMillis())); - return new WrappedResult(new AssetRegistrationFailed(registration.coordinates)); - } else if (throwable instanceof TimeoutException) { - ctx.getLog().warn("Rescheduling asset registration for {}", registration.coordinates); - timers.startSingleTimer(registration.coordinates, registration, Duration.ofSeconds(2)); - return new WrappedResult(new AssetRegistrationFailed(registration.coordinates)); - } - ctx.getLog().error("Failed to register asset for " + registration.coordinates.asStandardCoordinates(), throwable); - return new WrappedResult(new AssetRegistrationFailed(registration.coordinates)); - - } - return new WrappedResult(new AssetRegistrationCompleted(registration.coordinates)); - }); - return Behaviors.same(); - }) - .onMessage(WrappedResult.class, result -> { - final var response = result.response; - if (response instanceof AssetRegistrationCompleted a) { - ctx.getLog().debug("Successful Asset registration of {}", a.coordinates); - } else if (response instanceof AssetRegistrationFailed f) { - ctx.getLog().warn("Failed registration of {}", f.coordinates); - } - return Behaviors.same(); - }) - .build()) - ); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ArtifactSubscriber.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ArtifactSubscriber.java deleted file mode 100644 index dcb00fe0..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ArtifactSubscriber.java +++ /dev/null @@ -1,134 +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.synchronizer.gitmanaged; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Routers; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.japi.Pair; -import akka.stream.FlowShape; -import akka.stream.javadsl.Balance; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.GraphDSL; -import akka.stream.javadsl.Merge; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorFlow; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; -import org.spongepowered.synchronizer.akka.FlowUtil; -import org.spongepowered.synchronizer.gitmanaged.domain.GitCommand; -import org.spongepowered.synchronizer.gitmanaged.domain.GitManagedArtifact; -import org.spongepowered.synchronizer.gitmanaged.util.jgit.CommitResolutionManager; - -import java.net.URI; -import java.time.Duration; - -public final class ArtifactSubscriber { - - public static void setup(ArtifactService artifacts, ActorContext ctx) { - final Flow flow = generateFlows(ctx); - - artifacts.artifactUpdate() - .subscribe() - .atLeastOnce(flow); - } - - private static Flow generateFlows(ActorContext ctx) { - final var sharding = ClusterSharding.get(ctx.getSystem()); - final var repoAssociatedFlow = getRepoAssociatedFlow(sharding, ctx); - - return FlowUtil.splitClassFlows( - Pair.create(ArtifactUpdate.GitRepositoryAssociated.class, repoAssociatedFlow) - ); - } - - @SuppressWarnings("unchecked") - private static Flow getRepoAssociatedFlow( - ClusterSharding sharding, - ActorContext ctx - ) { - // Step 1 - Register the repository with GitManagedArtifact - final var registerRepo = Flow.fromFunction( - c -> sharding.entityRefFor(GitManagedArtifact.ENTITY_TYPE_KEY, c.coordinates().asMavenString()) - .ask( - replyTo -> new GitCommand.RegisterRepository(URI.create(c.repository()), replyTo), - Duration.ofSeconds(20) - ) - .thenApply(d -> c) - .toCompletableFuture() - .join() - ); - final var key = Routers.group(CommitResolutionManager.SERVICE_KEY); - final var workerRef = ctx.spawn(key, "repo-associated-commit-resolver"); - // Step 2 - Get unresolved commits from GitManagedArtifact to perform work - final Flow registerRepoFlow = Flow.create() - .mapAsync( - 1, c -> sharding.entityRefFor(GitManagedArtifact.ENTITY_TYPE_KEY, c.coordinates().asMavenString()) - .ask(GitCommand.GetUnresolvedVersions::new, Duration.ofSeconds(20)) - ) - .flatMapConcat(work -> { - final Map tuple2s = work.repositories().isEmpty() - ? HashMap.empty() - : work.unresolvedCommits(); - final List requests = tuple2s - .toList().map( - (t) -> new CommitResolutionManager.ResolveCommitDetails( - t._1(), t._2(), work.repositories(), null) - ); - return Source.from(requests); - }); - // Step 2a - Resolve the commits - final var flow = ActorFlow.ask( - 4, - workerRef, - Duration.ofMinutes(10), - (msg, replyTo) -> new CommitResolutionManager.ResolveCommitDetails( - msg.coordinates(), msg.commit(), msg.gitRepo(), replyTo) - ); - - return Flow.fromGraph(GraphDSL.create(b -> { - final var balance = b.add(Balance.create(4)); - final var zip = b.add(Merge.create(4)); - final var add = b.add(registerRepo.via(registerRepoFlow)); - b.from(add.out()) - .toFanOut(balance); - for (int i = 0; i < 4; i++) { - final var resolver = b.add(flow); - b.from(balance.out(i)) - .via(resolver) - .toInlet(zip.in(i)); - - } - return FlowShape.of(add.in(), zip.out()); - })); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/CommitConsumer.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/CommitConsumer.java deleted file mode 100644 index 3af605f2..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/CommitConsumer.java +++ /dev/null @@ -1,140 +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.synchronizer.gitmanaged; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.actor.typed.receptionist.Receptionist; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.typed.Cluster; -import akka.japi.Pair; -import akka.stream.javadsl.Flow; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.VersionedArtifactUpdates; -import org.spongepowered.synchronizer.actor.CommitDetailsRegistrar; -import org.spongepowered.synchronizer.actor.CommitRegistrar; -import org.spongepowered.synchronizer.akka.FlowUtil; -import org.spongepowered.synchronizer.gitmanaged.domain.GitCommand; -import org.spongepowered.synchronizer.gitmanaged.domain.GitManagedArtifact; -import org.spongepowered.synchronizer.gitmanaged.util.jgit.CommitResolutionManager; - -import java.time.Duration; -import java.util.UUID; - -public class CommitConsumer { - - public static void setupSubscribers(VersionsService versionsService, ActorContext ctx) { - final var member = Cluster.get(ctx.getSystem()).selfMember(); - final var uid = UUID.randomUUID(); - final var workerName = "commit-resolver-" + uid; - if (member.hasRole("commit-resolver")) { - final var registrar = ctx.spawn(CommitRegistrar.register(versionsService), "commit-registrar-" + uid); - spawnCommitResolver(ctx, uid, workerName, registrar); - } - final var versionedAssetFlows = CommitConsumer.createVersionedAssetFlows(ctx); - versionsService.versionedArtifactUpdatesTopic() - .subscribe() - .atLeastOnce(versionedAssetFlows); - } - - public static Flow createVersionedAssetFlows( - ActorContext ctx - ) { - final var sharding = ClusterSharding.get(ctx.getSystem()); - final var associateCommitFlow = registerRawCommitFlow(sharding); - - final var registerResolvedCommits = registerResolvedCommitDetails(sharding); - - return FlowUtil.splitClassFlows( - Pair.create(VersionedArtifactUpdates.CommitExtracted.class, associateCommitFlow), - Pair.create(VersionedArtifactUpdates.GitCommitDetailsAssociated.class, registerResolvedCommits) - ); - } - - private static void spawnCommitResolver( - ActorContext ctx, UUID uid, - String workerName, - final ActorRef registrar - ) { - final ActorRef workerRef; - - final var resolver = CommitResolutionManager.resolveCommit(registrar); - final var supervisedResolver = Behaviors.supervise(resolver) - .onFailure(SupervisorStrategy.restartWithBackoff( - Duration.ofMillis(100), - Duration.ofSeconds(40), - 0.1 - )); - final var pool = Routers.pool(4, supervisedResolver); - - workerRef = ctx.spawn( - pool, - workerName + "-" + uid, - DispatcherSelector.defaultDispatcher() - ); - // Announce it to the cluster - ctx.getSystem().receptionist().tell(Receptionist.register(CommitResolutionManager.SERVICE_KEY, workerRef)); - - } - - private static Flow registerRawCommitFlow( - ClusterSharding sharding - ) { - return Flow.fromFunction(msg -> sharding.entityRefFor( - GitManagedArtifact.ENTITY_TYPE_KEY, - msg.coordinates().asArtifactCoordinates().asMavenString() - ) - .ask( - replyTo -> new GitCommand.RegisterRawCommit(msg.coordinates(), msg.commit(), replyTo), - Duration.ofSeconds(20) - ) - .toCompletableFuture() - .join() - ); - } - - private static Flow registerResolvedCommitDetails( - ClusterSharding sharding - ) { - return Flow.fromFunction(event -> sharding - .entityRefFor( - GitManagedArtifact.ENTITY_TYPE_KEY, - event.coordinates().asArtifactCoordinates().asMavenString() - ) - .ask( - replyTo -> new GitCommand.MarkVersionAsResolved(event.coordinates(), event.commit(), replyTo), - Duration.ofSeconds(20) - ) - .toCompletableFuture() - .join() - ); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java deleted file mode 100644 index 404430c3..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/ScheduledCommitResolver.java +++ /dev/null @@ -1,302 +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.synchronizer.gitmanaged; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.actor.typed.receptionist.Receptionist; -import akka.actor.typed.receptionist.ServiceKey; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.typed.ClusterSingleton; -import akka.cluster.typed.SingletonActor; -import akka.japi.Pair; -import akka.stream.Graph; -import akka.stream.Materializer; -import akka.stream.SourceShape; -import akka.stream.javadsl.Balance; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.GraphDSL; -import akka.stream.javadsl.Merge; -import akka.stream.javadsl.Sink; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorFlow; -import akka.stream.typed.javadsl.ActorSink; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.Group; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.artifact.api.event.ArtifactUpdate; -import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; -import org.spongepowered.downloads.artifact.api.query.GroupsResponse; -import org.spongepowered.synchronizer.SonatypeSynchronizer; -import org.spongepowered.synchronizer.akka.FlowUtil; -import org.spongepowered.synchronizer.gitmanaged.domain.GitCommand; -import org.spongepowered.synchronizer.gitmanaged.domain.GitManagedArtifact; -import org.spongepowered.synchronizer.gitmanaged.util.jgit.CommitResolutionManager; - -import java.time.Duration; - -public final class ScheduledCommitResolver { - - public static final ServiceKey SCHEDULED_REFRESH = ServiceKey.create( - ScheduledRefresh.class, "scheduled-refresh"); - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(Refresh.class), - @JsonSubTypes.Type(Register.class), - @JsonSubTypes.Type(CommitResolved.class), - @JsonSubTypes.Type(WorkCompleted.class), - @JsonSubTypes.Type(Resync.class) - }) - public sealed interface ScheduledRefresh extends Jsonable { - } - - @JsonTypeName("refresh") - public record Refresh() implements ScheduledRefresh { - } - - @JsonTypeName("resync") - record Resync() implements ScheduledRefresh { - } - - @JsonTypeName("register-coordinates") - record Register(ArtifactCoordinates coordinates, ActorRef replyTo) implements ScheduledRefresh { - } - - @JsonTypeName("resolved") - record CommitResolved() implements ScheduledRefresh { - } - - @JsonTypeName("completed") - record WorkCompleted() implements ScheduledRefresh { - } - - public static void setup( - ArtifactService artifactService, - ActorContext context - ) { - final var singleton = SingletonActor.of(setup(artifactService), "ScheduledCommitResolver"); - final var scheduler = ClusterSingleton.get(context.getSystem()).init(singleton); - final var sync = registerForSync(scheduler); - artifactService.artifactUpdate() - .subscribe() - .atLeastOnce(FlowUtil.splitClassFlows(Pair.create(ArtifactUpdate.ArtifactRegistered.class, sync))); - } - - private static Source parseResponseIntoArtifacts( - ArtifactService artifactService, - GroupsResponse r - ) { - final Source groups; - if (r instanceof GroupsResponse.Available a) { - groups = Source.from(a.groups().map(Group::groupCoordinates)); - } else { - groups = Source.empty(); - } - final var groupsToArtifacts = Flow.create() - .mapConcat(g -> { - final var join = artifactService.getArtifacts(g).invoke().toCompletableFuture().join(); - if (join instanceof GetArtifactsResponse.ArtifactsAvailable aa) { - return aa.artifactIds().map(id -> new ArtifactCoordinates(g, id)); - } else { - return List.empty(); - } - }); - return groups.via(groupsToArtifacts); - } - - private static Flow registerForSync( - final ActorRef scheduler - ) { - return ActorFlow.ask( - scheduler, Duration.ofMinutes(1), (reg, replyTo) -> new Register(reg.coordinates(), replyTo)); - } - - private static Behavior setup( - final ArtifactService artifactService - ) { - return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> { - ctx.getSystem().receptionist().tell(Receptionist.register(SCHEDULED_REFRESH, ctx.getSelf())); - ctx.getLog().info("Starting ScheduledCommitResolver"); - timers.startSingleTimer("resync", new Resync(), Duration.ofSeconds(30)); - timers.startPeriodicTimer("refresh", new Refresh(), Duration.ofMinutes(1)); - ctx.getLog().info("Scheduled refresh every minute"); - - return Behaviors.receive(ScheduledRefresh.class) - .onMessage(Register.class, msg -> { - msg.replyTo.tell(Done.done()); - final var key = Routers.group(CommitResolutionManager.SERVICE_KEY); - final ActorRef workerRef = ctx.spawn( - key, "repo-associated-commit-resolver"); - return waiting(workerRef, HashSet.of(msg.coordinates()), artifactService); - }) - .onMessage(Refresh.class, msg -> Behaviors.same()) - .onMessage(Resync.class, msg -> resyncArtifactCoordinates(artifactService, ctx)) - .build(); - })); - } - - private static Behavior resyncArtifactCoordinates( - ArtifactService artifactService, ActorContext ctx - ) { - final Flow getGroups = Flow.fromFunction(s -> s.getGroups() - .invoke() - .toCompletableFuture() - .join()); - Source.single(artifactService) - .via(getGroups - .flatMapConcat((GroupsResponse r) -> parseResponseIntoArtifacts(artifactService, r))) - .via(ActorFlow.ask(ctx.getSelf(), Duration.ofSeconds(10), Register::new)) - .to(Sink.ignore()) - .run(ctx.getSystem()); - return Behaviors.same(); - } - - private static Behavior waiting( - final ActorRef workerRef, - final HashSet artifactsKeepingTracked, - final ArtifactService artifactService - ) { - return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> { - final ClusterSharding sharding = ClusterSharding.get(ctx.getSystem()); - final Materializer mat = Materializer.createMaterializer(ctx.getSystem()); - return Behaviors.receive(ScheduledRefresh.class) - .onMessage(Resync.class, msg -> resyncArtifactCoordinates(artifactService, ctx)) - .onMessage(Refresh.class, msg -> { - performRefresh(workerRef, artifactsKeepingTracked, ctx, sharding, mat); - return working(workerRef, artifactsKeepingTracked, artifactService); - }) - .onMessage(Register.class, msg -> { - msg.replyTo.tell(Done.done()); - return waiting(workerRef, artifactsKeepingTracked.add(msg.coordinates()), artifactService); - }) - .build(); - })); - } - - @SuppressWarnings("unchecked") - private static void performRefresh( - final ActorRef workerRef, - final HashSet artifactsKeepingTracked, - final ActorContext ctx, - ClusterSharding sharding, - Materializer mat - ) { - final Source artifactsToWorkOn = Source.from(artifactsKeepingTracked); - final Flow workFetcherFlow = getWork(sharding).log( - "work-fetcher"); - final Flow parseWorkFlow = Flow.create() - .log("parse-work") - .flatMapConcat(work -> { - final Map tuple2s = work.unresolvedCommits(); - final List requests = tuple2s - .toList().map( - (t) -> new CommitResolutionManager.ResolveCommitDetails( - t._1(), t._2(), work.repositories(), null) - ); - return Source.from(requests); - }); - final Flow commitResolverAskFlow = ActorFlow.ask( - 4, - workerRef, - Duration.ofMinutes(10), - (m, replyTo) -> new CommitResolutionManager.ResolveCommitDetails( - m.coordinates(), m.commit(), m.gitRepo(), replyTo) - ); - final Graph, NotUsed> graph = GraphDSL.create(b -> { - final SourceShape artifacts = b.add(artifactsToWorkOn); - final var balance = b.add(Balance.create(4)); - final var zip = b.add(Merge.create(4)); - final var getWork = b.add(workFetcherFlow); - final var parseWork = b.add(parseWorkFlow.async()); - b.from(artifacts.out()) - .via(getWork) - .via(parseWork) - .toFanOut(balance); - for (int i = 0; i < 4; i++) { - final var resolver = b.add(commitResolverAskFlow.async()); - b.from(balance.out(i)) - .via(resolver) - .toInlet(zip.in(i)); - } - return SourceShape.of(zip.out()); - }); - final var log = ctx.getLog(); - final Sink sink = ActorSink.actorRef( - ctx.getSelf(), new WorkCompleted(), t -> { - log.error("Got an error while resolving commits", t); - return new WorkCompleted(); - }); - Source.fromGraph(graph) - .via(Flow.fromFunction(d -> new CommitResolved())) - .to(sink) - .run(mat); - } - - private static Behavior working( - ActorRef workerRef, - final HashSet artifactsKeepingTracked, - final ArtifactService artifactService - ) { - return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> Behaviors.receive(ScheduledRefresh.class) - .onMessage(Resync.class, msg -> resyncArtifactCoordinates(artifactService, ctx)) - .onMessage(Refresh.class, msg -> Behaviors.same()) - .onMessage(Register.class, msg -> { - msg.replyTo.tell(Done.done()); - return working(workerRef, artifactsKeepingTracked.add(msg.coordinates()), artifactService - ); - }) - .onMessage(CommitResolved.class, msg -> Behaviors.same()) - .onMessage(WorkCompleted.class, msg -> { - timers.startSingleTimer(new Refresh(), Duration.ofMinutes(1)); - return waiting(workerRef, artifactsKeepingTracked, artifactService); - }) - .build())); - } - - private static Flow getWork(ClusterSharding sharding) { - return Flow.create() - .map(artifact -> sharding.entityRefFor(GitManagedArtifact.ENTITY_TYPE_KEY, artifact.asMavenString()) - .ask(GitCommand.GetUnresolvedVersions::new, Duration.ofSeconds(30)) - .toCompletableFuture() - .join()) - .filter(GitCommand.UnresolvedWork::hasWork); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitCommand.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitCommand.java deleted file mode 100644 index 7d0c2247..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitCommand.java +++ /dev/null @@ -1,144 +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.synchronizer.gitmanaged.domain; - -import akka.Done; -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 com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonDeserialize -@JsonSubTypes({ - @JsonSubTypes.Type(value = GitCommand.RegisterRepository.class, name = "register-repository"), - @JsonSubTypes.Type(value = GitCommand.RegisterRawCommit.class, name = "register-raw-commit"), - @JsonSubTypes.Type(value = GitCommand.GetRepositories.class, name = "get-repositories"), - @JsonSubTypes.Type(value = GitCommand.GetUnresolvedVersions.class, name = "get-unresolved-versions"), - @JsonSubTypes.Type(value = GitCommand.MarkVersionAsResolved.class, name = "mark-version-as-resolved"), - @JsonSubTypes.Type(value = GitCommand.MarkVersionAsUnresolveable.class, name = "mark-version-as-unresolveable"), -}) -public sealed interface GitCommand extends Jsonable { - - record RegisterRepository(URI repository, ActorRef replyTo) implements GitCommand { - @JsonCreator - public RegisterRepository { - } - } - - record RegisterRawCommit( - MavenCoordinates coordinates, - String commitSha, - ActorRef replyTo - ) implements GitCommand { - @JsonCreator - public RegisterRawCommit { - } - } - - @JsonDeserialize - @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) - sealed interface RepositoryResponse extends Jsonable { - List repositories(); - } - - @JsonDeserialize - record RepositoriesAvaiable(List repositories) implements RepositoryResponse { - @JsonCreator - public RepositoriesAvaiable { - } - } - - @JsonDeserialize - record NoRepositories() implements RepositoryResponse { - @JsonCreator - public NoRepositories { - } - - @Override - public List repositories() { - return List.empty(); - } - } - - record GetRepositories(ActorRef replyTo) implements GitCommand { - @JsonCreator - public GetRepositories { - } - } - - record GetUnresolvedVersions(ActorRef replyTo) implements GitCommand { - @JsonCreator - public GetUnresolvedVersions { - } - } - - record MarkVersionAsResolved( - MavenCoordinates coordinates, - VersionedCommit commit, - ActorRef replyTo - ) implements GitCommand { - @JsonCreator - public MarkVersionAsResolved { - } - } - - record MarkVersionAsUnresolveable( - ActorRef replyTo, - MavenCoordinates coordinates, - String commitSha - ) implements GitCommand {} - - - static final UnresolvedWork EMPTY = new UnresolvedWork(HashMap.empty(), List.empty()); - @JsonDeserialize - @JsonSerialize - record UnresolvedWork( - Map unresolvedCommits, - List repositories - ) implements Jsonable { - @JsonCreator - public UnresolvedWork { - } - - public boolean isEmpty() { - return unresolvedCommits.isEmpty(); - } - - public boolean hasWork() { - return !unresolvedCommits.isEmpty() && !this.repositories.isEmpty(); - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java deleted file mode 100644 index a4c63b9d..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitEvent.java +++ /dev/null @@ -1,83 +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.synchronizer.gitmanaged.domain; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = GitEvent.RepositoryRegistered.class), - @JsonSubTypes.Type(value = GitEvent.CommitRegistered.class), - @JsonSubTypes.Type(value = GitEvent.CommitResolved.class), -}) -public sealed interface GitEvent extends AggregateEvent, Jsonable { - - AggregateEventShards INSTANCE = AggregateEventTag.sharded(GitEvent.class, 3); - - @Override - default AggregateEventTagger aggregateTag() { - return INSTANCE; - } - - @JsonTypeName("repository-registered") - record RepositoryRegistered(URI repository) implements GitEvent { - @JsonCreator - public RepositoryRegistered { - } - } - @JsonTypeName("commit-extracted") - record CommitRegistered(MavenCoordinates coordinates, String commit) implements GitEvent { - @JsonCreator - public CommitRegistered { - } - } - @JsonTypeName("commit-resolved") - record CommitResolved(MavenCoordinates coordinates, VersionedCommit resolvedCommit) implements GitEvent { - @JsonCreator - public CommitResolved { - } - } - - @JsonTypeName("commit-unresolved") - record CommitUnresolvable(MavenCoordinates coordinates, String commit) implements GitEvent { - @JsonCreator - public CommitUnresolvable { - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitManagedArtifact.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitManagedArtifact.java deleted file mode 100644 index 361f0e11..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitManagedArtifact.java +++ /dev/null @@ -1,141 +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.synchronizer.gitmanaged.domain; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -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 akka.persistence.typed.javadsl.RetentionCriteria; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.synchronizer.gitmanaged.ScheduledCommitResolver; - -public class GitManagedArtifact extends EventSourcedBehaviorWithEnforcedReplies { - - public static final EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( - GitCommand.class, "git-managed-artifact"); - - private final ActorContext ctx; - private final ArtifactCoordinates coordinates; - private ActorRef scheduledRefresh; - - public static Behavior create(final PersistenceId persistenceId, final String entityId) { - return Behaviors.setup(ctx -> { - final var group = Routers.group(ScheduledCommitResolver.SCHEDULED_REFRESH); - final var scheduledRefresh = ctx.spawnAnonymous(group); - return new GitManagedArtifact(persistenceId, entityId, ctx, scheduledRefresh); - }); - } - - private GitManagedArtifact( - final PersistenceId persistenceId, - final String entityId, final ActorContext ctx, - final ActorRef scheduledRefresh - ) { - super(persistenceId); - this.scheduledRefresh = scheduledRefresh; - final var split = entityId.split(":"); - this.coordinates = new ArtifactCoordinates(split[0], split[1]); - this.ctx = ctx; - } - - @Override - public GitState emptyState() { - return new GitState.Empty(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - builder.forAnyState() - .onCommand(GitCommand.RegisterRepository.class, (state, cmd) -> this.Effect() - .persist(new GitEvent.RepositoryRegistered(cmd.repository())) - .thenRun(ns -> this.scheduledRefresh.tell(new ScheduledCommitResolver.Refresh())) - .thenReply(cmd.replyTo(), ns -> Done.done()) - ) - .onCommand(GitCommand.GetRepositories.class, (state, cmd) -> this.Effect() - .reply(cmd.replyTo(), state.repositories().isEmpty() ? - new GitCommand.NoRepositories() : new GitCommand.RepositoriesAvaiable(state.repositories())) - ) - .onCommand(GitCommand.GetUnresolvedVersions.class, (state, cmd) -> { - final var unresolvedWork = state.unresolvedVersions(); - this.ctx.getLog().debug("Unresolved versions: {}", unresolvedWork); - return this.Effect() - .reply(cmd.replyTo(), unresolvedWork); - } - ) - .onCommand(GitCommand.MarkVersionAsResolved.class, (state, cmd) -> this.Effect() - .persist(new GitEvent.CommitResolved(cmd.coordinates(), cmd.commit())) - .thenRun(() -> this.scheduledRefresh.tell(new ScheduledCommitResolver.Refresh())) - .thenReply(cmd.replyTo(), ns -> Done.done()) - ) - .onCommand(GitCommand.MarkVersionAsUnresolveable.class, (state, cmd) -> this.Effect() - .persist(new GitEvent.CommitUnresolvable(cmd.coordinates(), cmd.commitSha())) - .thenRun(() -> this.scheduledRefresh.tell(new ScheduledCommitResolver.Refresh())) - .thenReply(cmd.replyTo(), ns -> Done.done())) - .onCommand(GitCommand.RegisterRawCommit.class, (state, cmd) -> this.Effect() - .persist(new GitEvent.CommitRegistered(cmd.coordinates(), cmd.commitSha())) - .thenRun(() -> this.scheduledRefresh.tell(new ScheduledCommitResolver.Refresh())) - .thenReply(cmd.replyTo(), ns -> Done.done()) - ) - ; - return builder.build(); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - builder.forAnyState() - .onEvent( - GitEvent.RepositoryRegistered.class, - (state, event) -> state.withRepository(event.repository()) - ) - .onEvent( - GitEvent.CommitResolved.class, - (state, event) -> state.withResolvedVersion(event.coordinates(), event.resolvedCommit()) - ) - .onEvent( - GitEvent.CommitRegistered.class, - (state, event) -> state.withRawCommit(event.coordinates(), event.commit()) - ).onEvent( - GitEvent.CommitUnresolvable.class, - (state, event) -> state.withUnresolvedVersion(event.coordinates(), event.commit()) - ) - ; - return builder.build(); - } - - @Override - public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(10, 2); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitState.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitState.java deleted file mode 100644 index 520ccc3a..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/domain/GitState.java +++ /dev/null @@ -1,222 +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.synchronizer.gitmanaged.domain; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import io.vavr.collection.TreeMap; -import org.apache.maven.artifact.versioning.ComparableVersion; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; -import java.util.Comparator; -import java.util.Optional; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(GitState.Empty.class), - @JsonSubTypes.Type(GitState.Registered.class) -}) -public sealed interface GitState extends Jsonable { - - Comparator LATEST_COMPARATOR = Comparator.comparing( - m -> new ComparableVersion(m.version)) - .reversed(); - - GitState withRepository(URI repository); - - List repositories(); - - GitCommand.UnresolvedWork unresolvedVersions(); - - GitState withResolvedVersion( - MavenCoordinates coordinates, final VersionedCommit commit - ); - - GitState withRawCommit(MavenCoordinates coordinates, String commitSha); - - GitState withUnresolvedVersion(MavenCoordinates coordinates, final String commit); - - @JsonTypeName("empty") - final record Empty() implements GitState { - @JsonCreator - public Empty { - } - - @Override - public GitState withRepository(final URI repository) { - return new Registered( - List.of(repository), TreeMap.empty(LATEST_COMPARATOR), - HashMap.empty() - ); - } - - @Override - public List repositories() { - return List.empty(); - } - - @Override - public GitCommand.UnresolvedWork unresolvedVersions() { - return GitCommand.EMPTY; - } - - @Override - public GitState withResolvedVersion( - final MavenCoordinates coordinates, - final VersionedCommit commit - ) { - return new Registered( - List.empty(), - TreeMap.empty(LATEST_COMPARATOR), - HashMap.of(coordinates, commit) - ); - } - - @Override - public GitState withRawCommit(final MavenCoordinates coordinates, final String commitSha) { - return new Registered( - List.empty(), - TreeMap.of(LATEST_COMPARATOR, coordinates, Optional.of(commitSha)), - HashMap.empty() - ); - } - - @Override - public GitState withUnresolvedVersion( - final MavenCoordinates coordinates, - final String commit - ) { - return new Registered( - List.empty(), - TreeMap.of(LATEST_COMPARATOR, coordinates, Optional.of(commit)), - HashMap.empty(), - HashMap.of(coordinates, commit) - ); - } - } - - @JsonTypeName("registered") - final record Registered( - List repository, - Map> commits, - Map resolved, - Map unresolvable - ) implements GitState { - @JsonCreator - public Registered { - } - - public Registered( - final List repository, - final Map> commits, - final Map resolved - ) { - this(repository, commits, resolved, HashMap.empty()); - } - - @Override - public GitState withRepository(final URI repository) { - final var repos = this.repository.toSet().add(repository).toList(); - // Reset unresolved commits by re-merging with the existing set of commits - final var newCommits = this.commits.merge( - this.unresolvable.mapValues(Optional::of), (a, b) -> a.isEmpty() ? b : a); - return new Registered(repos, newCommits, this.resolved, HashMap.empty()); - } - - @Override - public List repositories() { - return this.repository; - } - - @Override - public GitCommand.UnresolvedWork unresolvedVersions() { - if (this.repository.isEmpty()) { - return GitCommand.EMPTY; - } - final var unresolvedCommits = this.commits.filterValues(Optional::isPresent) - .mapValues(Optional::get); - // Try to get the latest to work with in the first place - final var versionsWithCommits = unresolvedCommits - .toSortedMap(LATEST_COMPARATOR.reversed(), t -> t._1, t -> t._2) - .take(16); - return new GitCommand.UnresolvedWork(versionsWithCommits, this.repository); - } - - @Override - public GitState withResolvedVersion( - final MavenCoordinates coordinates, - final VersionedCommit commit - ) { - if (this.resolved.containsKey(coordinates)) { - return this; - } - final var newCommits = this.commits.remove(coordinates); - final var newResolved = this.resolved.put(coordinates, commit); - final var newUnresolved = this.unresolvable.remove(coordinates); - return new Registered(this.repository, newCommits, newResolved, newUnresolved); - } - - @Override - public GitState withRawCommit(final MavenCoordinates coordinates, final String commitSha) { - if (this.resolved.containsKey(coordinates)) { - return this; - } - final var newCommits = this.commits.put(coordinates, Optional.of(commitSha)); - return new Registered(this.repository, newCommits, this.resolved); - } - - @Override - public GitState withUnresolvedVersion( - final MavenCoordinates coordinates, - final String commit - ) { - if (this.resolved.containsKey(coordinates)) { - return this; - } - if (this.unresolvable.containsKey(coordinates)) { - return this; - } - final var newUnresolvable = this.unresolvable.put(coordinates, commit); - final var newCommits = this.commits.remove(coordinates); - return new Registered( - this.repository, - newCommits, - this.resolved, - newUnresolvable - ); - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/ActorLoggerPrinterWriter.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/ActorLoggerPrinterWriter.java deleted file mode 100644 index 0123f08c..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/ActorLoggerPrinterWriter.java +++ /dev/null @@ -1,121 +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.synchronizer.gitmanaged.util.jgit; - -import org.eclipse.jgit.lib.BatchingProgressMonitor; -import org.eclipse.jgit.lib.ProgressMonitor; -import org.slf4j.Logger; - -import java.util.Objects; - -public final class ActorLoggerPrinterWriter extends BatchingProgressMonitor implements ProgressMonitor { - private final Logger logger; - - public ActorLoggerPrinterWriter(Logger logger) { - this.logger = logger; - } - - @Override - protected void onUpdate(final String taskName, final int workCurr) { - StringBuilder s = new StringBuilder(); - format(s, taskName, workCurr); - if (this.logger.isDebugEnabled()) { - this.logger.debug(s.toString()); - } - } - - private void format(StringBuilder s, String taskName, int workCurr) { - s.append("\r"); //$NON-NLS-1$ - s.append(taskName); - s.append(": "); //$NON-NLS-1$ - while (s.length() < 25) - s.append(' '); - s.append(workCurr); - } - @Override - protected void onEndTask(final String taskName, final int workCurr) { - StringBuilder s = new StringBuilder(); - format(s, taskName, workCurr); - if (this.logger.isDebugEnabled()) { - this.logger.debug(s.toString()); - } - } - - @Override - protected void onUpdate(final String taskName, final int workCurr, final int workTotal, final int percentDone) { - StringBuilder s = new StringBuilder(); - format(s, taskName, workCurr, workTotal, percentDone); - } - - @Override - protected void onEndTask(final String taskName, final int workCurr, final int workTotal, final int percentDone) { - StringBuilder s = new StringBuilder(); - format(s, taskName, workCurr, workTotal, percentDone); - } - - private void format(StringBuilder s, String taskName, int cmp, - int totalWork, int pcnt) { - s.append("\r"); //$NON-NLS-1$ - s.append(taskName); - s.append(": "); //$NON-NLS-1$ - while (s.length() < 25) - s.append(' '); - - String endStr = String.valueOf(totalWork); - StringBuilder curStr = new StringBuilder(String.valueOf(cmp)); - while (curStr.length() < endStr.length()) - curStr.insert(0, " "); //$NON-NLS-1$ - if (pcnt < 100) - s.append(' '); - if (pcnt < 10) - s.append(' '); - s.append(pcnt); - s.append("% ("); //$NON-NLS-1$ - s.append(curStr); - s.append("/"); //$NON-NLS-1$ - s.append(endStr); - s.append(")"); //$NON-NLS-1$ - } - - @Override - public boolean equals(Object obj) { - if (obj == this) return true; - if (obj == null || obj.getClass() != this.getClass()) return false; - var that = (ActorLoggerPrinterWriter) obj; - return Objects.equals(this.logger, that.logger); - } - - @Override - public int hashCode() { - return Objects.hash(logger); - } - - @Override - public String toString() { - return "ActorLoggerPrinterWriter[" + - "logger=" + logger + ']'; - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/AssetCommitResolver.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/AssetCommitResolver.java deleted file mode 100644 index b6c7d9d2..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/AssetCommitResolver.java +++ /dev/null @@ -1,178 +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.synchronizer.gitmanaged.util.jgit; - -import akka.NotUsed; -import akka.actor.typed.javadsl.ActorContext; -import akka.japi.Pair; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.Source; -import io.vavr.CheckedFunction1; -import io.vavr.Tuple3; -import io.vavr.collection.List; -import io.vavr.control.Try; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.SubmoduleConfig; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.TagOpt; -import org.slf4j.Logger; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Collections; -import java.util.Optional; - -public final class AssetCommitResolver { - - - static Source startCommitResolution( - final ActorContext ctx, - final CommitResolutionManager.ArtifactRepoState newState, - final CommitResolutionManager.ResolveCommitDetails request - ) { - final var log = ctx.getLog(); - final var checkedOut = newState.checkedOut(); - final var commitSha = request.commit(); - return Source.from(Collections.singleton(ObjectId.fromString(commitSha))) - .mapConcat(objectId -> checkedOut.map(tuple -> tuple.append(objectId))) - .async() - .via(tryResolvingCommitFromGitDirectory(request.coordinates(), log)) - .async() - .via(parseResponse(request)); - } - - sealed interface CommitResolutionResponse { - } - - record CommitResolved( - URI repo, VersionedCommit commit - ) implements CommitResolutionResponse { - } - - public record CommitNotFound(MavenCoordinates coordinates, List uris, String commit) implements CommitResolutionResponse { - } - - - static Flow>, CommitResolutionResponse, NotUsed> parseResponse( - CommitResolutionManager.ResolveCommitDetails request - ) { - return Flow.>, CommitResolutionResponse>fromFunction( - opt -> - opt.map(pair -> new CommitResolved(pair.first(), pair.second())) - .orElseGet(() -> new CommitNotFound(request.coordinates(), request.gitRepo(), request.commit())) - ); - } - - static Flow, Optional>, NotUsed> tryResolvingCommitFromGitDirectory( - MavenCoordinates coordinates, Logger log - ) { - return Flow., Optional>>fromFunction(tuple3 -> { - if (log.isDebugEnabled()) { - log.debug("Resolving commit {} for {}", tuple3._3.getName(), coordinates); - log.debug("Opening Git directory {}", tuple3._2); - } - final var directory = tuple3._2().toFile(); - final var git = Git.open(directory); - final var fetchCmd = git.fetch() - .setRecurseSubmodules(SubmoduleConfig.FetchRecurseSubmodulesMode.YES) - .setTagOpt(TagOpt.FETCH_TAGS) - .setRemoveDeletedRefs(false); - fetchCmd.call(); - - return Try.withResources(() -> new RevWalk(git.getRepository())) - .of(tryFetchAndResolveCommit(coordinates, log, tuple3._3)) - .map(convertCommitToVersionedCommit(log, tuple3._1)) - .map(Optional::of) - .getOrElseGet(t -> { - log.error("Failed to resolve commit {} for {}", tuple3._3.getName(), coordinates); - return Optional.empty(); - }); - }); - } - - static CheckedFunction1 tryFetchAndResolveCommit( - MavenCoordinates coordinates, Logger log, ObjectId objectId - ) { - return revWalk -> { - final var commit = revWalk.lookupCommit(objectId); - revWalk.parseBody(commit); - if (log.isTraceEnabled()) { - log.trace("Commit Body Parsed {}", commit); - } - revWalk.parseHeaders(commit); - if (log.isTraceEnabled()) { - log.trace("Commit Headers Parsed {}", commit.getShortMessage()); - } - final var commitTime = commit.getCommitTime(); - if (log.isTraceEnabled()) { - log.trace("Commit Revision {}", commit.getCommitTime()); - } - if (log.isDebugEnabled()) { - log.debug( - "{} at {} has {}", coordinates, objectId.name(), commitTime); - } - return commit; - }; - } - - static java.util.function.Function> convertCommitToVersionedCommit( - Logger log, URI repo - ) { - return commit -> { - if (log.isTraceEnabled()) { - log.trace("Commit Resolved {}", commit); - } - final var commitMessage = commit.getShortMessage(); - final var commitBody = commit.getFullMessage(); - final var commitId = commit.getId().getName(); - final var commitDate = commit.getCommitTime(); - final var instant = Instant.ofEpochSecond(commitDate); - final var committerIdent = commit.getCommitterIdent(); - final var committer = new VersionedCommit.Commiter( - committerIdent.getName(), committerIdent.getEmailAddress()); - final var authorIdent = commit.getAuthorIdent(); - final var author = new VersionedCommit.Author( - authorIdent.getName(), authorIdent.getEmailAddress()); - final var timeZone = committerIdent - .getTimeZone(); - final var commitDateTime = instant.atZone(timeZone.toZoneId()); - final var commitUrl = URI.create(repo.toString() + "/commit/" + commitId); - return Pair.apply(repo, new VersionedCommit( - commitMessage, - commitBody, - commitId, - author, - committer, - Optional.of(commitUrl), - commitDateTime - )); - }; - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/CommitResolutionManager.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/CommitResolutionManager.java deleted file mode 100644 index 71e77786..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/CommitResolutionManager.java +++ /dev/null @@ -1,410 +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.synchronizer.gitmanaged.util.jgit; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.receptionist.ServiceKey; -import akka.japi.function.Function2; -import akka.stream.Materializer; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.Sink; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorSink; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import io.vavr.collection.Set; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.synchronizer.actor.CommitDetailsRegistrar; - -import java.net.URI; -import java.nio.file.Path; -import java.time.Duration; -import java.util.UUID; -import java.util.function.Predicate; - -/** - * An {@link akka.actor.Actor} that accepts a Versioned Artifact with a commit - * sha to resolve the - * underlying Git Commit information (like the commit message, author, etc.) as - * the sha's are being resolved. - */ -public final class CommitResolutionManager { - - public static final ServiceKey SERVICE_KEY = ServiceKey.create(Command.class, "commit-resolver"); - - - @JsonDeserialize - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(ResolveCommitDetails.class), - }) - public sealed interface Command extends Jsonable { - } - - @JsonTypeName("resolve-commit-details") - public record ResolveCommitDetails( - MavenCoordinates coordinates, - String commit, - List gitRepo, - ActorRef replyTo - ) implements Command { - - @JsonCreator - public ResolveCommitDetails { - } - } - - public static Behavior resolveCommit(ActorRef registrar) { - return Behaviors.setup(ctx -> { - final var clonerBehavior = Behaviors.supervise(RepositoryCloner.cloner()) - .onFailure(SupervisorStrategy.restart()); - StubSystemReader.init(); - final var uuid = UUID.randomUUID(); - final var cloner = ctx.spawn(clonerBehavior, "repository-cloner-" + uuid); - final var materializer = Materializer.createMaterializer(ctx); - return awaiting(cloner, registrar, materializer, ClonedRepoState.EMPTY); - }); - } - - private static Behavior awaiting( - final ActorRef cloner, - final ActorRef registrar, - final Materializer materializer, - final ClonedRepoState state - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage(ResolveCommitDetails.class, msg -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Resolving commit details for {}", msg.coordinates); - } - final var repoState = state.repositoriesCloned.get(msg.coordinates.asArtifactCoordinates()); - if (repoState.isEmpty()) { - if (msg.gitRepo.isEmpty()) { - msg.replyTo.tell(Done.done()); - return Behaviors.same(); - } - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug("[{}] Cloning {}", msg.coordinates, msg.gitRepo); - } - ctx.ask( - RepositoryCloner.CloneResponse.class, - cloner, - Duration.ofMinutes(20), - replyTo -> new RepositoryCloner.CloneRepos( - msg.coordinates.asArtifactCoordinates(), msg.gitRepo, replyTo), - handleCloneResponse(ctx, msg, state) - ); - return waitingForClones(cloner, registrar, materializer, state, List.empty()); - } - final var artifactRepoState = repoState.get(); - final var unclonedRepos = artifactRepoState.repositories.filter(Predicate.not(artifactRepoState.checkedOut::containsKey)); - final var newUncloned = msg.gitRepo.filter(Predicate.not(artifactRepoState.checkedOut::containsKey)); - final var allUncloned = unclonedRepos.addAll(newUncloned); - if (!allUncloned.isEmpty()) { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug("[{}] Cloning {}", msg.coordinates.asStandardCoordinates(), allUncloned); - } - ctx.ask( - RepositoryCloner.CloneResponse.class, - cloner, - Duration.ofMinutes(20), - replyTo -> new RepositoryCloner.CloneRepos( - msg.coordinates.asArtifactCoordinates(), allUncloned.toList(), replyTo), - handleCloneResponse(ctx, msg, state) - ); - - return waitingForClones(cloner, registrar, materializer, state, List.empty()); - } - - startCommitResolution(materializer, ctx, msg, artifactRepoState); - - return waitingForResolution(cloner, registrar, materializer, state, List.empty()); - }) - .build() - ); - } - - private static Behavior waitingForClones( - final ActorRef cloner, - final ActorRef registrar, - final Materializer materializer, - final ClonedRepoState state, - final List queue - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage( - ResolveCommitDetails.class, - msg -> waitingForClones(cloner, registrar, materializer, state, queue.append(msg)) - ) - .onMessage(FailedCheckout.class, msg -> { - msg.msg.replyTo.tell(Done.done()); - if (queue.isEmpty()) { - return awaiting(cloner, registrar, materializer, state); - } - final var head = queue.head(); - ctx.ask( - RepositoryCloner.CloneResponse.class, - cloner, - Duration.ofMinutes(20), - replyTo -> new RepositoryCloner.CloneRepos( - head.coordinates.asArtifactCoordinates(), head.gitRepo, replyTo), - handleCloneResponse(ctx, head, state) - ); - queue.tail(); - return waitingForClones(cloner, registrar, materializer, state, queue); - }) - .onMessage(RepositoriesClonedReadyToResolve.class, msg -> { - if (msg.checkedOut.isEmpty()) { - ctx.getLog().warn( - "[{}] No repositories successfully checked out with {}", msg.msg.coordinates, msg.msg.gitRepo); - msg.msg.replyTo.tell(Done.done()); - return completeResolution(cloner, registrar, materializer, state, queue, ctx); - } - final ClonedRepoState newState = state.withClonedRepos(msg); - final var request = msg.msg; - final var repos = newState.repositoriesCloned.get( - request.coordinates.asArtifactCoordinates()).get(); - startCommitResolution(materializer, ctx, request, repos); - - return waitingForResolution(cloner, registrar, materializer, newState, queue); - }) - .build()); - } - - private static Behavior waitingForResolution( - final ActorRef cloner, - final ActorRef registrar, - final Materializer materializer, - final ClonedRepoState state, - final List queue - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage( - ResolveCommitDetails.class, - msg -> waitingForResolution(cloner, registrar, materializer, state, queue.append(msg)) - ) - .onMessage(RepositoriesClonedReadyToResolve.class, msg -> { - final ClonedRepoState newState = state.withClonedRepos(msg); - return waitingForResolution(cloner, registrar, materializer, newState, queue.append(msg.msg)); - }) - .onMessage(WrappedCommitResolutionResult.class, msg -> { - final var request = msg.msg; - final var result = msg.result; - if (result instanceof AssetCommitResolver.CommitNotFound notFound) { - registrar.tell( - new CommitDetailsRegistrar.CommitNotFound( - notFound.uris().head(), - notFound.commit(), - request.coordinates, - msg.msg.replyTo - ) - ); - } else if (result instanceof AssetCommitResolver.CommitResolved resolved) { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().info("[{}] Resolved commit {}", request.coordinates, resolved.commit().link()); - } - final var commit = resolved.commit(); - - registrar.tell( - new CommitDetailsRegistrar.HandleVersionedCommitReport(resolved.repo(), commit, - request.coordinates, - request.replyTo - )); - } - return Behaviors.same(); - }) - .onMessage(CompletedResolutionAttempts.class, msg -> { - ctx.getLog().info("[{}] Completed commit {} resolution", msg.msg.coordinates.asStandardCoordinates(), msg.msg.commit); - msg.msg.replyTo.tell(Done.done()); - return completeResolution(cloner, registrar, materializer, state, queue, ctx); - }) - .onMessage(ExceptionallyCompletedResultion.class, msg -> { - msg.msg.replyTo.tell(Done.done()); - return completeResolution(cloner, registrar, materializer, state, queue, ctx); - }) - .build() - ); - } - - private static Behavior completeResolution( - ActorRef cloner, ActorRef registrar, - Materializer materializer, ClonedRepoState state, List queue, ActorContext ctx - ) { - if (queue.isEmpty()) { - return awaiting(cloner, registrar, materializer, state); - } - final var head = queue.head(); - final var artifactCoordinates = head.coordinates.asArtifactCoordinates(); - if (state.repositoriesCloned.get(artifactCoordinates).isEmpty()) { - ctx.ask( - RepositoryCloner.CloneResponse.class, - cloner, - Duration.ofMinutes(20), - replyTo -> new RepositoryCloner.CloneRepos( - head.coordinates.asArtifactCoordinates(), head.gitRepo, replyTo), - handleCloneResponse(ctx, head, state) - ); - return waitingForClones(cloner, registrar, materializer, state, queue.tail()); - } - final var artifactRepoStates = state.repositoriesCloned.get(artifactCoordinates).get(); - - startCommitResolution(materializer, ctx, head, artifactRepoStates); - - return waitingForResolution(cloner, registrar, materializer, state, queue.tail()); - } - - - private static Function2 handleCloneResponse( - ActorContext ctx, ResolveCommitDetails msg, ClonedRepoState empty - ) { - return (reply, throwable) -> { - if (throwable != null) { - ctx.getLog().error("Failed to clone repo", throwable); - return new FailedCheckout(msg, empty); - } - if (reply instanceof RepositoryCloner.SuccessfullyCloned sc) { - return new RepositoriesClonedReadyToResolve( - msg, - new ClonedRepoState(HashMap.empty()), - sc.checkedOut() - ); - } - return new FailedCheckout(msg, empty); - }; - } - - private static void startCommitResolution( - Materializer materializer, ActorContext ctx, ResolveCommitDetails head, - ArtifactRepoState artifactRepoStates - ) { - final var flow = interpretResult(head); - final Source via = AssetCommitResolver.startCommitResolution(ctx, artifactRepoStates, head) - .async() - .via(flow); - final Sink sink = ActorSink.actorRef( - ctx.getSelf(), - new CompletedResolutionAttempts(head), - t -> new ExceptionallyCompletedResultion(head, t) - ); - via.to(sink).run(materializer); - } - - private static Flow interpretResult( - ResolveCommitDetails msg - ) { - return Flow.fromFunction( - response -> new WrappedCommitResolutionResult( - msg, response)); - } - - @JsonDeserialize - public record ArtifactRepoState( - Set repositories, - Map checkedOut, - Set inUse - ) implements Jsonable { - @JsonCreator - public ArtifactRepoState { - } - } - - @JsonDeserialize - public record ClonedRepoState( - Map repositoriesCloned - ) implements Jsonable { - static final ClonedRepoState EMPTY = new ClonedRepoState(HashMap.empty()); - - @JsonCreator - public ClonedRepoState { - } - - public ClonedRepoState withClonedRepos(RepositoriesClonedReadyToResolve msg) { - final var coordinates = msg.msg.coordinates.asArtifactCoordinates(); - - if (this.repositoriesCloned.containsKey(coordinates)) { - return this; - } - final var artifactState = new ArtifactRepoState( - msg.msg.gitRepo.toSet(), - msg.checkedOut, - msg.checkedOut.keySet() - ); - final var newRepositories = this.repositoriesCloned.put(coordinates, artifactState); - return new ClonedRepoState(newRepositories); - } - - public ClonedRepoState reset() { - return new ClonedRepoState(this.repositoriesCloned.mapValues(s -> new ArtifactRepoState( - s.repositories, - s.checkedOut, - HashSet.empty() - ))); - } - } - - private record RepositoriesClonedReadyToResolve( - ResolveCommitDetails msg, - ClonedRepoState state, - Map checkedOut - ) implements Command { - } - - private record FailedCheckout(ResolveCommitDetails msg, ClonedRepoState state) implements Command { - } - - private record WrappedCommitResolutionResult( - ResolveCommitDetails msg, - AssetCommitResolver.CommitResolutionResponse result - ) implements Command { - } - - private record CompletedResolutionAttempts( - ResolveCommitDetails msg - ) implements Command { - } - - private record ExceptionallyCompletedResultion( - ResolveCommitDetails msg, - Throwable error - ) implements Command { - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/FileWalkerConsumer.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/FileWalkerConsumer.java deleted file mode 100644 index 5b2a56be..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/FileWalkerConsumer.java +++ /dev/null @@ -1,68 +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.synchronizer.gitmanaged.util.jgit; - -import akka.actor.typed.ActorRef; -import io.vavr.CheckedConsumer; - -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.function.Function; - -public final class FileWalkerConsumer extends SimpleFileVisitor { - - private final CheckedConsumer acceptFile; - - public static FileWalkerConsumer tell(final ActorRef ref, Function messageCreator) { - return new FileWalkerConsumer(path -> ref.tell(messageCreator.apply(path))); - } - - public FileWalkerConsumer(final CheckedConsumer acceptFile) { - this.acceptFile = acceptFile; - } - - @Override - public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { - try { - acceptFile.accept(file); - } catch (Throwable e) { - throw new IOException(e); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException { - try { - acceptFile.accept(dir); - } catch (Throwable e) { - throw new IOException(e); - } - return FileVisitResult.CONTINUE; - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java deleted file mode 100644 index 62af456e..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/RepositoryCloner.java +++ /dev/null @@ -1,270 +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.synchronizer.gitmanaged.util.jgit; - -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.japi.Pair; -import akka.stream.Materializer; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.Sink; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorSink; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashMap; -import io.vavr.collection.HashSet; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import io.vavr.collection.Set; -import io.vavr.control.Either; -import io.vavr.control.Try; -import org.eclipse.jgit.api.Git; -import org.slf4j.Logger; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.UUID; -import java.util.function.Function; - -public final class RepositoryCloner { - - sealed interface CloneCommand extends Jsonable { - } - - sealed interface CloneResponse extends Jsonable { - } - - record CloneRepos( - ArtifactCoordinates coordinates, - List urls, - ActorRef replyTo - ) implements CloneCommand { - } - - public static Behavior cloner() { - return Behaviors.setup(ctx -> { - final var materializer = Materializer.createMaterializer(ctx); - return waiting(materializer); - }); - } - - private static Behavior waiting(Materializer materializer) { - return Behaviors.setup(ctx -> Behaviors.receive(CloneCommand.class) - .onMessage(CloneRepos.class, msg -> { - callClone(ctx, materializer, msg); - return cloning(new CloneState(msg, msg.coordinates), materializer, List.empty()); - }) - .build()); - } - - record SuccessfullyCloned( - ArtifactCoordinates coordinates, - Map checkedOut - ) implements CloneResponse { - } - - record PartialClone( - ArtifactCoordinates coordinates, - Map checkedOut - ) implements CloneResponse { - } - - private record CloneState( - CloneRepos command, - ArtifactCoordinates coordinates, - Set completed, - Map checkedOut, - Set failed - ) { - - public CloneState(CloneRepos msg, ArtifactCoordinates coordinates) { - this(msg, coordinates, HashSet.empty(), HashMap.empty(), HashSet.empty()); - } - - CloneState withFailed(URI uri) { - if (this.completed.contains(uri)) { - return this; - } - return new CloneState( - this.command, - this.coordinates, - this.completed.add(uri), - this.checkedOut, - this.failed.add(uri) - ); - } - - CloneState withSucceeded(URI repo, Path path) { - if (this.completed.contains(repo)) { - return this; - } - return new CloneState( - this.command, - this.coordinates, - this.completed.add(repo), - this.checkedOut.put(repo, path), - this.failed - ); - } - } - - - private static Behavior cloning(CloneState state, Materializer materializer, List queue) { - return Behaviors.setup(ctx -> Behaviors.receive(CloneCommand.class) - .onMessage(CloneRepos.class, msg -> cloning(state, materializer, queue.append(msg))) - .onMessage(CloneFailed.class, msg -> { - ctx.getLog().warn(String.format("[%s] Clone failed for %s", msg.coordinates, msg.repo), msg.cause); - final var newState = state.withFailed(msg.repo); - return cloning(newState, materializer, queue); - }) - .onMessage(CloneSucceeded.class, msg -> { - final var newState = state.withSucceeded(msg.repo, msg.path); - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug("Clone Succeeded for {}", msg.repo); - } - return cloning(newState, materializer, queue); - }) - .onMessage(CloneFailedCompletion.class, msg -> { - ctx.getLog().warn("[{}] Clone failed for {}", msg.coordinates, msg.cause); - state.command.replyTo.tell(new PartialClone(state.coordinates, state.checkedOut)); - return cloneNextOrWait(materializer, queue, ctx); - }) - .onMessage(CloneCompleted.class, msg -> { - ctx.getLog().info("Clone completed for {}", state.coordinates); - state.command.replyTo.tell(new SuccessfullyCloned(state.coordinates, state.checkedOut)); - return cloneNextOrWait(materializer, queue, ctx); - }) - .build()); - } - - private static Behavior cloneNextOrWait( - final Materializer materializer, final List queue, - final ActorContext ctx - ) { - if (queue.isEmpty()) { - return waiting(materializer); - } - final var head = queue.head(); - final var nextState = new CloneState( - head, - head.coordinates - ); - callClone(ctx, materializer, head); - return cloning(nextState, materializer, queue.tail()); - } - - private static void callClone(ActorContext ctx, Materializer materializer, CloneRepos head) { - final Source urls = Source.from(head.urls); - final var log = ctx.getLog(); - final Flow flow = Flow.fromFunction(url -> cloneRepo(log, head.coordinates, url) - .map(path -> { - if (log.isDebugEnabled()) { - log.debug("[{}] Cloned {} to {}", head.coordinates.asMavenString(), url, path); - } - return new CloneSucceeded(head.coordinates, url, path); - }) - .mapLeft(uri -> { - if (log.isDebugEnabled()) { - log.debug("[{}] Clone failed for {}", head.coordinates.asMavenString(), uri); - } - return new CloneFailed(head.coordinates, uri.first(), uri.second()); - }) - .fold(Function.identity(), Function.identity()) - ); - final Sink sink = ActorSink.actorRef( - ctx.getSelf(), new CloneCompleted(head.coordinates), - t -> new CloneFailedCompletion(head.coordinates, t) - ); - urls - .via(flow) - .to(sink) - .run(materializer); - } - - - private record CloneFailed( - ArtifactCoordinates coordinates, - URI repo, - Throwable cause - ) implements CloneCommand { - } - - private record CloneFailedCompletion( - ArtifactCoordinates coordinates, - Throwable cause - ) implements CloneCommand { - - } - - private record CloneSucceeded( - ArtifactCoordinates coordinates, - URI repo, - Path path - ) implements CloneCommand { - } - - private record CloneCompleted(ArtifactCoordinates coordinates) implements CloneCommand { - } - - private static Either, Path> cloneRepo( - final Logger log, - final ArtifactCoordinates coordinates, - final URI remoteRepo - ) { - final var tempdirPrefix = String.format( - "soad-%s-%s", coordinates.artifactId(), - UUID.randomUUID() - ); - final var repoDirectory = Try.of(() -> Files.createTempDirectory( - tempdirPrefix - )); - - if (log.isTraceEnabled()) { - log.trace("Preparing directory for checkout {}", repoDirectory); - } - final var writer = new ActorLoggerPrinterWriter(log); - final var cloneCommand = repoDirectory.mapTry(Path::toFile) - .map(directory -> Git.cloneRepository() - .setDirectory(directory) - .setProgressMonitor(writer) - .setCloneAllBranches(false) - .setCloneSubmodules(true) - .setURI(remoteRepo.toString()) - ); - if (log.isTraceEnabled()) { - log.trace("Checking out {} to {}", remoteRepo, repoDirectory); - } - return cloneCommand.mapTry(org.eclipse.jgit.api.CloneCommand::call) - .flatMapTry(repo -> repoDirectory) - .toEither() - .mapLeft(t -> Pair.create(remoteRepo, t)); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/StubSystemReader.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/StubSystemReader.java deleted file mode 100644 index 01232b26..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/gitmanaged/util/jgit/StubSystemReader.java +++ /dev/null @@ -1,124 +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.synchronizer.gitmanaged.util.jgit; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.SystemReader; - -import java.io.File; -import java.net.URL; - -public class StubSystemReader extends SystemReader { - - private static final class Holder { - private static final URL dummyGitConfig = Holder.class.getClassLoader().getResource( - "soad.gitconfig"); - private static final File gitConfig = new File(dummyGitConfig.getFile()); - static final SystemReader INSTANCE = new StubSystemReader(gitConfig); - static { - SystemReader.setInstance(INSTANCE); - } - } - - public static SystemReader getInstance() { - return Holder.INSTANCE; - } - - public static void init() { - final var instance = getInstance(); - if (SystemReader.getInstance() != instance) { - SystemReader.setInstance(instance); - } - } - - private static final SystemReader proxy = SystemReader.getInstance(); - private final File userGitConfig; - - public StubSystemReader(File userGitConfig) { - super(); - this.userGitConfig = userGitConfig; - } - - @Override - public String getenv(String variable) { - return proxy.getenv(variable); - } - - @Override - public String getHostname() { - return proxy.getHostname(); - } - - @Override - public String getProperty(String key) { - return proxy.getProperty(key); - } - - @Override - public long getCurrentTime() { - return proxy.getCurrentTime(); - } - - @Override - public int getTimezone(long when) { - return proxy.getTimezone(when); - } - - @Override - public FileBasedConfig openUserConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, userGitConfig, fs); - } - - // Return an empty system configuration, based on example in SystemReader.Default#openSystemConfig - @Override - public FileBasedConfig openSystemConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, this.userGitConfig, fs) { - @Override - public void load() { - } - - @Override - public boolean isOutdated() { - return false; - } - }; - } - - @Override - public FileBasedConfig openJGitConfig(final Config parent, final FS fs) { - return new FileBasedConfig(parent, this.userGitConfig, fs) { - @Override - public void load() { - } - - @Override - public boolean isOutdated() { - return false; - } - }; - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/RequestArtifactsToSync.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/RequestArtifactsToSync.java deleted file mode 100644 index b787fbd3..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/RequestArtifactsToSync.java +++ /dev/null @@ -1,90 +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.synchronizer.resync; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.query.GetArtifactsResponse; - -public class RequestArtifactsToSync { - - @JsonDeserialize - @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) - public sealed interface Command extends Jsonable { - } - - public record GatherGroupArtifacts( - String groupCoordinates, - ActorRef replyTo - ) implements Command { - } - - private record WrappedArtifactsToSync( - ArtifactsToSync result, - ActorRef replyTo - ) implements Command { - } - - public record ArtifactsToSync( - List artifactsNeeded - ) { - } - - public static Behavior create( - final ArtifactService artifactService - ) { - return Behaviors.setup(ctx -> Behaviors.receive(RequestArtifactsToSync.Command.class) - .onMessage(GatherGroupArtifacts.class, (g) -> { - final var listCompletableFuture = artifactService.getArtifacts(g.groupCoordinates).invoke() - .thenApply(artifactsResponse -> { - if (!(artifactsResponse instanceof GetArtifactsResponse.ArtifactsAvailable a)) { - return List.empty(); - } - return a.artifactIds() - .map(id -> new ArtifactCoordinates(g.groupCoordinates, id)); - }); - ctx.pipeToSelf(listCompletableFuture, (ok, exception) -> { - if (exception == null) { - return new WrappedArtifactsToSync(new ArtifactsToSync(ok), g.replyTo); - } - return new WrappedArtifactsToSync(new ArtifactsToSync(List.empty()), g.replyTo); - }); - return Behaviors.same(); - }) - .onMessage(WrappedArtifactsToSync.class, (w) -> { - w.replyTo.tell(w.result); - return Behaviors.same(); - }) - .build()); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncExtension.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncExtension.java deleted file mode 100644 index a058f185..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncExtension.java +++ /dev/null @@ -1,44 +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.synchronizer.resync; - -import akka.actor.AbstractExtensionId; -import akka.actor.ExtendedActorSystem; -import akka.actor.ExtensionIdProvider; - -public class ResyncExtension extends AbstractExtensionId - implements ExtensionIdProvider { - public static final ResyncExtension SettingsProvider = new ResyncExtension(); - - @Override - public ResyncSettings createExtension(final ExtendedActorSystem system) { - return new ResyncSettings(system.settings().config().getConfig("systemofadownload.synchronizer.worker.resync")); - } - - @Override - public ResyncExtension lookup() { - return SettingsProvider; - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java deleted file mode 100644 index 5251200a..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncManager.java +++ /dev/null @@ -1,148 +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.synchronizer.resync; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.Behavior; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.stream.javadsl.Flow; -import akka.stream.javadsl.Sink; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorFlow; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.Group; -import org.spongepowered.downloads.artifact.api.query.GroupsResponse; -import org.spongepowered.synchronizer.SynchronizerSettings; -import org.spongepowered.synchronizer.actor.ArtifactSyncWorker; - -import java.time.Duration; -import java.util.UUID; -import java.util.concurrent.CompletionStage; - -public final class ResyncManager { - - sealed interface Resync { - } - - record PerformResync() implements Resync { - } - - record ArtifactsToSync(List artifacts) implements Resync { - } - - public static Behavior create( - final ArtifactService artifactService, - final SynchronizerSettings.VersionSync versionSync - ) { - return Behaviors.withTimers(t -> { - t.startTimerWithFixedDelay("resync", new PerformResync(), versionSync.interval); - t.startSingleTimer("start", new PerformResync(), versionSync.startupDelay); - return setup(artifactService); - }); - } - - private static Behavior setup( - final ArtifactService artifactService - ) { - return Behaviors.setup(ctx -> { - final var sharding = ClusterSharding.get(ctx.getSystem()); - final var dispatch = DispatcherSelector.defaultDispatcher(); - final var requester = ctx.spawn( - Behaviors.supervise(RequestArtifactsToSync.create(artifactService)).onFailure( - SupervisorStrategy.restart()), - String.format("requester-%s-%d", UUID.randomUUID(), System.currentTimeMillis()), - dispatch - ); - final var artifactSyncPool = Routers.pool( - 4, - Behaviors.supervise(ArtifactSyncWorker.create(sharding)) - .onFailure( - SupervisorStrategy.restartWithBackoff(Duration.ofSeconds(1), Duration.ofMinutes(10), 0.2)) - ); - final var syncWorker = ctx.spawn( - artifactSyncPool, - String.format("pooled-artifact-synchronization-workers-%s-%d", UUID.randomUUID(), System.currentTimeMillis()), - dispatch - ); - final var requestFlow = ActorFlow.ask( - requester, - Duration.ofSeconds(30), - RequestArtifactsToSync.GatherGroupArtifacts::new - ); - - final var registrationFlow = ActorFlow.ask( - syncWorker, - Duration.ofMinutes(20), - ArtifactSyncWorker.PerformResync::new - ); - return awaiting(artifactService, requestFlow, registrationFlow); - }); - } - - private static Behavior awaiting( - final ArtifactService artifactService, - final Flow requestFlow, - final Flow registrationFlow - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Resync.class) - .onMessage(ArtifactsToSync.class, result -> { - Source.from(result.artifacts) - .async() - .via(registrationFlow.async()) - .runWith(Sink.ignore(), ctx.getSystem()); - return Behaviors.same(); - }) - .onMessage(PerformResync.class, g -> { - final var makeRequest = artifactService.getGroups() - .invoke() - .thenApply(groups -> ((GroupsResponse.Available) groups).groups()) - .thenCompose(groups -> { - final Sink, CompletionStage>> fold = Sink.fold( - List.empty(), List::appendAll); - return Source.from(groups.map(Group::groupCoordinates).asJava()) - .async() - .via(requestFlow) - .map(RequestArtifactsToSync.ArtifactsToSync::artifactsNeeded) - .runWith(fold, ctx.getSystem()); - }); - ctx.pipeToSelf(makeRequest, (ok, exception) -> { - if (exception != null) { - ctx.getLog().error("Failed to process sync", exception); - } - return new ArtifactsToSync(ok); - }); - return Behaviors.same(); - }) - .build()); - - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncSettings.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncSettings.java deleted file mode 100644 index 85533b3f..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/ResyncSettings.java +++ /dev/null @@ -1,45 +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.synchronizer.resync; - -import akka.actor.Extension; -import com.typesafe.config.Config; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -public class ResyncSettings implements Extension { - public final String repository; - public final Duration timeout; - public final int retryCount; - public final String agentName; - - public ResyncSettings(Config config) { - this.repository = config.getString("repository"); - this.retryCount = config.getInt("retry"); - this.agentName = config.getString("agent-name"); - this.timeout = Duration.ofSeconds(config.getDuration("timeout", TimeUnit.SECONDS)); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java deleted file mode 100644 index 7bb8d69d..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/ArtifactSynchronizerAggregate.java +++ /dev/null @@ -1,204 +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.synchronizer.resync.domain; - -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 akka.persistence.typed.javadsl.ReplyEffect; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; -import io.vavr.collection.List; -import io.vavr.control.Try; -import io.vavr.jackson.datatype.VavrModule; -import org.spongepowered.downloads.maven.artifact.ArtifactMavenMetadata; -import org.spongepowered.synchronizer.resync.ResyncExtension; -import org.spongepowered.synchronizer.resync.ResyncSettings; - -import javax.xml.stream.XMLInputFactory; -import java.io.Serial; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.StringJoiner; -import java.util.concurrent.CompletableFuture; - -public final class ArtifactSynchronizerAggregate - extends EventSourcedBehaviorWithEnforcedReplies { - public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create(Command.class, "ArtifactSynchronizer"); - private final ActorContext ctx; - private final ResyncSettings settings; - private final HttpClient httpClient; - private final XmlMapper mapper; - - public ArtifactSynchronizerAggregate( - final EntityContext context, final ActorContext ctx, - final ResyncSettings settings - ) { - 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 - )); - this.ctx = ctx; - this.settings = settings; - final var mapper = new XmlMapper(); - mapper.registerModule(new VavrModule()); - this.httpClient = HttpClient.newBuilder() - .connectTimeout(this.settings.timeout) - .version(HttpClient.Version.HTTP_2) - .build(); - this.mapper = mapper; - } - - public static Behavior create(EntityContext context) { - return Behaviors.setup(ctx -> { - final ResyncSettings settings = ResyncExtension.SettingsProvider.get(ctx.getSystem()); - - return new ArtifactSynchronizerAggregate(context, ctx, settings); - }); - } - - @Override - public SyncState emptyState() { - return SyncState.EMPTY; - } - - @Override - public EventHandler eventHandler() { - final var builder = newEventHandlerBuilder() - .forAnyState() - .onEvent( - SynchronizeEvent.SynchronizedArtifacts.class, - (event) -> new SyncState(event.updatedTime(), event.metadata()) - ); - return builder.build(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder() - .forAnyState() - .onCommand(Command.Resync.class, this::handleResync) - .onCommand(Command.WrappedResync.class, this::handleResponse); - return builder.build(); - } - - private ReplyEffect handleResponse(SyncState state, Command.WrappedResync cmd) { - if (cmd.response() instanceof Command.Failed) { - return this.Effect().reply(cmd.replyTo(), List.empty()); - } - if (cmd.response() instanceof Command.Completed c) { - final var metadata = c.metadata(); - if (metadata.versioning().lastUpdated.equals(state.lastUpdated)) { - final var versionsToSync = state.versions.versioning() - .versions.map(state.coordinates()::version); - return this.Effect() - .reply(cmd.replyTo(), versionsToSync); - } - return this.Effect() - .persist(new SynchronizeEvent.SynchronizedArtifacts(metadata, metadata.versioning().lastUpdated)) - .thenReply(cmd.replyTo(), s -> s.versions.versioning().versions.map(s.coordinates()::version)); - } - return this.Effect().reply(cmd.replyTo(), List.empty()); - } - - - private ReplyEffect handleResync(SyncState state, Command.Resync cmd) { - final var groupId = !state.groupId.equals(cmd.coordinates().groupId()) - ? cmd.coordinates().groupId() - : state.groupId; - final var artifactId = !state.artifactId.equals(cmd.coordinates().artifactId()) - ? cmd.coordinates().artifactId() - : state.artifactId; - ctx.pipeToSelf( - getArtifactMetadata(groupId, artifactId), - (response, throwable) -> { - if (throwable != null) { - if (throwable instanceof UnsupportedArtifactException ua) { - this.ctx.getLog().warn( - String.format("Unsupported artifact %s", state.coordinates()), - throwable - ); - } else { - this.ctx.getLog().error( - String.format("Unable to get maven-metadata.xml for artifact: %s", state.coordinates()), - throwable - ); - } - return new Command.WrappedResync(new Command.Failed(), cmd.replyTo()); - } - return new Command.WrappedResync(new Command.Completed(response), cmd.replyTo()); - } - ); - return this.Effect().noReply(); - } - - private static final class UnsupportedArtifactException extends Exception { - - - @Serial private static final long serialVersionUID = 4579607644429804821L; - - public UnsupportedArtifactException(final String artifact, final String url) { - super(String.format("Unsupported artifact by id %s using url: %s", artifact, url)); - } - } - - private CompletableFuture getArtifactMetadata(String groupId, String artifactId) { - final var url = new StringJoiner("/", this.settings.repository, "") - .add(groupId.replace(".", "/")) - .add(artifactId) - .add("maven-metadata.xml") - .toString(); - return Try.of(() -> URI.create(url)) - .map(uri -> HttpRequest.newBuilder() - .uri(uri) - .header("User-Agent", this.settings.agentName) - .build()) - .toCompletableFuture() - .thenCompose(request -> this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) - .thenCompose(response -> Try.of(() -> response) - .filterTry( - r -> r.statusCode() == 200, - () -> new UnsupportedArtifactException(groupId + ":" + artifactId, url) - ) - .toCompletableFuture() - ) - .thenApply(HttpResponse::body) - .thenApply(is -> Try.of(() -> XMLInputFactory.newFactory().createXMLStreamReader(is)).get()) - .thenCompose(reader -> Try.of( - () -> this.mapper.readValue(reader, ArtifactMavenMetadata.class)).toCompletableFuture()) - ); - } - -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/Command.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/Command.java deleted file mode 100644 index 8a36be72..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/Command.java +++ /dev/null @@ -1,82 +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.synchronizer.resync.domain; - -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 com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.maven.artifact.ArtifactMavenMetadata; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = Command.Resync.class, name = "resync"), - @JsonSubTypes.Type(value = Command.Failed.class, name = "failed"), - @JsonSubTypes.Type(value = Command.WrappedResync.class, name = "wrapped-resync"), - @JsonSubTypes.Type(value = Command.Completed.class, name = "completed"), -}) -public interface Command extends Jsonable { - @JsonDeserialize - record Resync( - ArtifactCoordinates coordinates, - ActorRef> replyTo - ) implements Command { - @JsonCreator - public Resync {} - - } - - record Completed(ArtifactMavenMetadata metadata) implements Response { - @JsonCreator - public Completed { - } - } - - @JsonDeserialize - sealed interface Response extends Command { - } - - @JsonDeserialize - record Failed() implements Response { - @JsonCreator - public Failed { - } - } - - record WrappedResync( - Response response, - ActorRef> replyTo - ) implements Response { - @JsonCreator - public WrappedResync { - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SyncState.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SyncState.java deleted file mode 100644 index a8cdbaff..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SyncState.java +++ /dev/null @@ -1,60 +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.synchronizer.resync.domain; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.maven.artifact.ArtifactMavenMetadata; -import org.spongepowered.downloads.maven.artifact.Versioning; - -@JsonDeserialize -final class SyncState implements Jsonable { - - public final String groupId; - public final String artifactId; - public final String lastUpdated; - public final ArtifactMavenMetadata versions; - - @JsonIgnore - static final SyncState EMPTY = new SyncState("", new ArtifactMavenMetadata("", "", new Versioning())); - - @JsonCreator - public SyncState( - final String lastUpdated, - final ArtifactMavenMetadata versions - ) { - this.lastUpdated = lastUpdated; - this.versions = versions; - this.groupId = versions.groupId(); - this.artifactId = versions.artifactId(); - } - - ArtifactCoordinates coordinates() { - return new ArtifactCoordinates(this.groupId, this.artifactId); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SynchronizeEvent.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SynchronizeEvent.java deleted file mode 100644 index ea505c28..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/resync/domain/SynchronizeEvent.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.synchronizer.resync.domain; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.maven.artifact.ArtifactMavenMetadata; - -interface SynchronizeEvent extends Jsonable, AggregateEvent { - - AggregateEventShards TAG = AggregateEventTag.sharded(SynchronizeEvent.class, 3); - - @Override - default AggregateEventTagger aggregateTag() { - return TAG; - } - - @JsonDeserialize - record SynchronizedArtifacts( - ArtifactMavenMetadata metadata, - String updatedTime - ) implements SynchronizeEvent { - @JsonCreator - public SynchronizedArtifacts {} - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactConsumer.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactConsumer.java deleted file mode 100644 index 1fddd092..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactConsumer.java +++ /dev/null @@ -1,81 +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.synchronizer.versionsync; - -import akka.Done; -import akka.NotUsed; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Routers; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.stream.javadsl.Flow; -import akka.stream.typed.javadsl.ActorFlow; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.event.GroupUpdate; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.synchronizer.SonatypeSynchronizer; -import org.spongepowered.synchronizer.SynchronizerSettings; -import org.spongepowered.synchronizer.actor.ArtifactSyncWorker; - -public final class ArtifactConsumer { - public static void subscribeToArtifactUpdates( - final ActorContext context, - final ArtifactService artifactService, - final VersionsService versionsService, - final ClusterSharding clusterSharding, - final SynchronizerSettings settings - ) { - // region Synchronize Artifact Versions from Maven - final AuthUtils auth = AuthUtils.configure(context.getSystem().settings().config()); - ArtifactVersionSyncModule.setup(context, clusterSharding, auth, versionsService); - - final var syncWorkerBehavior = ArtifactSyncWorker.create(clusterSharding); - final var pool = Routers.pool( - settings.reactiveSync.poolSize, - syncWorkerBehavior - ); - - final var registrationRef = context.spawn( - pool, - "group-event-subscriber", - DispatcherSelector.defaultDispatcher() - ); - final Flow actorAsk = ActorFlow.ask( - settings.reactiveSync.parallelism, - registrationRef, settings.reactiveSync.timeOut, - (g, b) -> { - if (!(g instanceof GroupUpdate.ArtifactRegistered a)) { - return new ArtifactSyncWorker.Ignored(b); - } - return new ArtifactSyncWorker.PerformResync(a.coordinates(), b); - } - ); - artifactService.groupTopic() - .subscribe() - .atLeastOnce(actorAsk); - // endregion - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncEntity.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncEntity.java deleted file mode 100644 index e4675ae9..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncEntity.java +++ /dev/null @@ -1,307 +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.synchronizer.versionsync; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.actor.typed.javadsl.TimerScheduler; -import akka.cluster.sharding.typed.javadsl.EntityTypeKey; -import akka.pattern.CircuitBreakerOpenException; -import akka.persistence.typed.PersistenceId; -import akka.persistence.typed.RecoveryCompleted; -import akka.persistence.typed.javadsl.CommandHandlerWithReply; -import akka.persistence.typed.javadsl.EventHandler; -import akka.persistence.typed.javadsl.EventSourcedBehaviorWithEnforcedReplies; -import akka.persistence.typed.javadsl.ReplyEffect; -import akka.persistence.typed.javadsl.RetentionCriteria; -import akka.persistence.typed.javadsl.SignalHandler; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; - -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeoutException; -import java.util.function.BiFunction; -import java.util.function.Supplier; - -public class ArtifactVersionSyncEntity - extends EventSourcedBehaviorWithEnforcedReplies { - - public static final EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( - SyncRegistration.class, "artifact-version-sync"); - - private final ActorContext ctx; - private final TimerScheduler timers; - private final AuthUtils auth; - private final VersionsService service; - private final ActorRef batchSync; - - public static Behavior create( - final AuthUtils auth, final VersionsService service, final PersistenceId persistenceId - ) { - - return Behaviors.setup(ctx -> { - final var router = Routers.group(BatchVersionSyncManager.KEY); - final var ref = ctx.spawnAnonymous(router); - return Behaviors.withTimers( - timers -> new ArtifactVersionSyncEntity(ctx, timers, auth, service, persistenceId, ref)); - }); - } - - public ArtifactVersionSyncEntity( - final ActorContext ctx, - final TimerScheduler timers, - final AuthUtils auth, - final VersionsService service, - final PersistenceId persistenceId, - final ActorRef ref - ) { - super(persistenceId); - this.ctx = ctx; - this.timers = timers; - this.auth = auth; - this.service = service; - this.batchSync = ref; - } - - @Override - public VersionRegistrationState emptyState() { - return new VersionRegistrationState.Empty(); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - builder.forAnyState() - .onEvent( - VersionSyncEvent.RegisteredBatch.class, - (state, evt) -> { - this.batchSync.tell(new BatchVersionSyncManager.ArtifactToSync(evt.artifact())); - return state.acceptBatch(evt.artifact(), evt.coordinates()); - } - ) - .onEvent(VersionSyncEvent.RegisteredVersion.class, (state, evt) -> { - this.batchSync.tell( - new BatchVersionSyncManager.ArtifactToSync(evt.coordinates().asArtifactCoordinates())); - return state.acceptVersion(evt.coordinates()); - }) - .onEvent(VersionSyncEvent.ResolvedVersion.class, (state, evt) -> { - this.batchSync.tell( - new BatchVersionSyncManager.ArtifactToSync(evt.coordinates().asArtifactCoordinates())); - return state.resolvedVersion(evt.coordinates()); - }) - .onEvent(VersionSyncEvent.StartedBatchRegistration.class, (state, evt) -> state.startBatch(evt.batched())) - .onEvent(VersionSyncEvent.FailedVersion.class, (state, evt) -> state.failedVersion(evt.coordinates())) - ; - return builder.build(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - builder.forAnyState() - .onCommand(SyncRegistration.Register.class, (state, cmd) -> { - if (state.hasVersion(cmd.coordinates())) { - return this.Effect() - .noReply(); - } - return this.Effect() - .persist(new VersionSyncEvent.RegisteredVersion(cmd.coordinates())) - .thenRun( - () -> this.timers.startSingleTimer( - cmd.coordinates(), SyncRegistration.Timeout.INSTANCE, Duration.ofSeconds(1))) - .thenNoReply(); - }) - .onCommand(SyncRegistration.MarkRegistered.class, (state, cmd) -> this.Effect() - .persist(new VersionSyncEvent.ResolvedVersion(cmd.coordinates())) - .thenRun(this::checkIfStillHasPending) - .thenNoReply() - ) - .onCommand(SyncRegistration.DelayRegistration.class, (state, cmd) -> { - this.timers.startSingleTimer( - cmd.coordinates(), new SyncRegistration.RetryFailed(cmd.coordinates()), cmd.duration()); - return this.Effect().noReply(); - }) - .onCommand(SyncRegistration.RetryFailed.class, (state, cmd) -> { - final var request = this.createRegistrationRequest(cmd.coordinates()); - this.ctx.pipeToSelf(request.serviceCall().get(), request.onComplete()::apply); - return this.Effect() - .noReply(); - }) - .onCommand(SyncRegistration.Refresh.class, (state, cmd) -> { - final List pending = state.getPending(); - if (state.isActive() && !pending.isEmpty()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug("Still awaiting for versions to complete registration: {}", pending); - } - pending.forEachWithIndex((coordinates, index) -> this.timers.startSingleTimer(coordinates, - new SyncRegistration.RetryFailed(coordinates), Duration.ofSeconds((int) (1 + (0.2 * index))) - )); - this.timers.startSingleTimer("refresh", SyncRegistration.Refresh.INSTANCE, Duration.ofSeconds(20 + pending.size())); - return this.Effect().noReply(); - } - return this.registerVersionsInBatches(state); - }) - .onCommand(SyncRegistration.AlreadyRegistered.class, (state, cmd) -> this.Effect() - .persist(new VersionSyncEvent.ResolvedVersion(cmd.coordinates())) - .thenRun(this::checkIfStillHasPending) - .thenNoReply() - ) - .onCommand(SyncRegistration.GroupUnregistered.class, (state, cmd) -> this.Effect() - .persist(new VersionSyncEvent.FailedVersion(cmd.coordinates())) - .thenRun(this::checkIfStillHasPending) - .thenNoReply() - ) - .onCommand(SyncRegistration.SyncBatch.class, (state, cmd) -> this.Effect() - .persist(new VersionSyncEvent.RegisteredBatch(cmd.artifact(), cmd.coordinates())) - .thenRun(() -> this.timers.startSingleTimer("timeout", SyncRegistration.Timeout.INSTANCE, - Duration.ofSeconds(1) - )) - .thenReply(cmd.replyTo(), ns -> Done.done())) - .onCommand(SyncRegistration.Timeout.class, (state, cmd) -> this.registerVersionsInBatches(state)) - ; - return builder.build(); - } - - private void checkIfStillHasPending(VersionRegistrationState state) { - // Go ahead and ask the state to update for the next batch - if (!state.isActive()) { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("No more pending versions, scheduling a refresh in 500 milliseconds"); - } - this.timers.startSingleTimer("refresh", SyncRegistration.Refresh.INSTANCE, Duration.ofMillis(500)); - } else { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("State is currently active, {}", state.getPending()); - } - } - } - - private ReplyEffect registerVersionsInBatches( - VersionRegistrationState state - ) { - if (state.isActive() && !state.getPending().isEmpty()) { - final var pending = state.getPending(); - this.ctx.getLog().warn("Resubmitting version registration due to pending versions: {}", pending); - pending.map(this::createRegistrationRequest).forEach( - p -> this.ctx.pipeToSelf(p.serviceCall().get(), p.onComplete::apply)); - return this.Effect().noReply(); - } - if (!state.getPending().isEmpty()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug("Rescheduling Refresh due to Pending registrations: {}", state.getPending()); - } - this.timers.startSingleTimer("refresh", SyncRegistration.Refresh.INSTANCE, Duration.ofSeconds(2)); - this.batchSync.tell(new BatchVersionSyncManager.ArtifactToSync(state.coordinates())); - return this.Effect().noReply(); - } - final List batched = state.getNextBatch(); - if (batched.isEmpty()) { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("No more pending batches"); - } - return this.Effect().noReply(); - } - final var map = batched.map(this::createRegistrationRequest); - return this.Effect() - .persist(new VersionSyncEvent.StartedBatchRegistration(batched)) - .thenRun(ns -> map.forEach(p -> this.ctx.pipeToSelf(p.serviceCall().get(), p.onComplete()::apply))) - .thenRun( - ns -> this.timers.startSingleTimer("refresh", SyncRegistration.Refresh.INSTANCE, Duration.ofSeconds(2))) - .thenRun(ns -> this.batchSync.tell(new BatchVersionSyncManager.ArtifactToSync(ns.coordinates()))) - .thenNoReply(); - } - - private VersionRegistrationParams createRegistrationRequest(MavenCoordinates c) { - - final var serviceCallSupplier = (Supplier>) () -> this.auth.internalAuth( - this.service.registerArtifactCollection(c.groupId, c.artifactId)) - .invoke(new VersionRegistration.Register.Version(c)) - .toCompletableFuture(); - final var onComplete = (BiFunction) (ok, failure) -> { - if (failure != null) { - if (failure instanceof CompletionException ce) { - failure = ce.getCause(); - } - if (failure instanceof CircuitBreakerOpenException cbe) { - this.ctx.getLog().warn("Circuit breaker is open, delaying registration of {}", c); - return new SyncRegistration.DelayRegistration( - c, Duration.ofMillis(cbe.remainingDuration().toMillis()).plus(Duration.ofSeconds(4))); - } else if (failure instanceof TimeoutException) { - ctx.getLog().warn("Rescheduling asset registration for {}", c); - return new SyncRegistration.DelayRegistration(c, Duration.ofSeconds(2)); - } - ctx.getLog().error( - String.format( - "Received error trying to synchronize %s", - c.asStandardCoordinates() - ), failure); - return new SyncRegistration.RetryFailed(c); - } - if (ok instanceof VersionRegistration.Response.ArtifactAlreadyRegistered a) { - ctx.getLog().trace("Redundant registration of {}", a.coordinates()); - return new SyncRegistration.AlreadyRegistered(c); - } else if (ok instanceof VersionRegistration.Response.GroupMissing gm) { - ctx.getLog().error("Group missing for {}", gm.groupId()); - return new SyncRegistration.GroupUnregistered(c); - } else if (ok instanceof VersionRegistration.Response.RegisteredArtifact r) { - ctx.getLog().trace("Successful registration of {}", r.mavenCoordinates()); - return new SyncRegistration.MarkRegistered(c); - } - ctx.getLog().warn("Failed registration synchronizing {} with response {}", c, ok); - return new SyncRegistration.RetryFailed(c); - }; - return new VersionRegistrationParams(serviceCallSupplier, onComplete); - } - - record VersionRegistrationParams( - Supplier> serviceCall, - BiFunction onComplete - ) { - } - - @Override - public SignalHandler signalHandler() { - final var builder = this.newSignalHandlerBuilder(); - // Enable restarting our timers - builder.onSignal(RecoveryCompleted.class, (state, signal) -> { - this.timers.startSingleTimer("refresh", SyncRegistration.Refresh.INSTANCE, Duration.ofMillis(500)); - }); - return super.signalHandler(); - } - - @Override - public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(100, 2); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncModule.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncModule.java deleted file mode 100644 index b8f1658a..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/ArtifactVersionSyncModule.java +++ /dev/null @@ -1,57 +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.synchronizer.versionsync; - -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import akka.persistence.typed.PersistenceId; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.synchronizer.SonatypeSynchronizer; - -public class ArtifactVersionSyncModule { - - public static void setup( - final ActorContext ctx, - final ClusterSharding sharding, - final AuthUtils authUtils, - final VersionsService service - ) { - ctx.spawnAnonymous(Behaviors.supervise(BatchVersionSyncManager.setup()) - .onFailure(SupervisorStrategy.resume())); - sharding.init(Entity.of( - ArtifactVersionSyncEntity.ENTITY_TYPE_KEY, - context -> ArtifactVersionSyncEntity.create(authUtils, service, - PersistenceId.of( - context.getEntityTypeKey().name(), - context.getEntityId() - ) - ) - )); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/BatchVersionSyncManager.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/BatchVersionSyncManager.java deleted file mode 100644 index e277da25..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/BatchVersionSyncManager.java +++ /dev/null @@ -1,89 +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.synchronizer.versionsync; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.receptionist.Receptionist; -import akka.actor.typed.receptionist.ServiceKey; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashSet; -import io.vavr.collection.Set; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -import java.time.Duration; - -public final class BatchVersionSyncManager { - - static final ServiceKey KEY = ServiceKey.create(Command.class, "batch-version-sync"); - - interface Command extends Jsonable { - } - - public record ArtifactToSync(ArtifactCoordinates coordinates) implements Command { - } - - public enum Refresh implements Command { - INSTANCE - } - - public static Behavior setup() { - return Behaviors.setup(ctx -> { - ctx.getSystem().receptionist().tell(Receptionist.register(KEY, ctx.getSelf())); - return timedSync(HashSet.empty()); - }); - } - - private static Behavior timedSync(Set coordinates) { - return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> { - timers.startSingleTimer("refresh", Refresh.INSTANCE, Duration.ofSeconds(20)); - return Behaviors.receive(Command.class) - .onMessage(ArtifactToSync.class, msg -> { - ctx.getLog().info("Received artifact to sync: {}", msg.coordinates); - if (coordinates.contains(msg.coordinates)) { - return Behaviors.same(); - } - final var sharding = ClusterSharding.get(ctx.getSystem()); - sharding.entityRefFor(ArtifactVersionSyncEntity.ENTITY_TYPE_KEY, msg.coordinates.asMavenString()) - .tell(SyncRegistration.Refresh.INSTANCE); - return timedSync(coordinates.add(msg.coordinates)); - }) - .onMessage(Refresh.class, msg -> { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug("Refreshing all artifacts: {}", coordinates); - } - final var sharding = ClusterSharding.get(ctx.getSystem()); - coordinates.forEach( - c -> sharding.entityRefFor(ArtifactVersionSyncEntity.ENTITY_TYPE_KEY, c.asMavenString()).tell( - SyncRegistration.Refresh.INSTANCE) - ); - timers.startSingleTimer("refresh", Refresh.INSTANCE, Duration.ofMinutes(1)); - return Behaviors.same(); - }) - .build(); - })); - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/SyncRegistration.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/SyncRegistration.java deleted file mode 100644 index ac626136..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/SyncRegistration.java +++ /dev/null @@ -1,93 +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.synchronizer.versionsync; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.time.Duration; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(SyncRegistration.Register.class), - @JsonSubTypes.Type(SyncRegistration.SyncBatch.class), - @JsonSubTypes.Type(SyncRegistration.AlreadyRegistered.class), - @JsonSubTypes.Type(SyncRegistration.Timeout.class), - @JsonSubTypes.Type(SyncRegistration.GroupUnregistered.class), - @JsonSubTypes.Type(SyncRegistration.MarkRegistered.class), - @JsonSubTypes.Type(SyncRegistration.Refresh.class), - @JsonSubTypes.Type(SyncRegistration.RetryFailed.class) -}) -public sealed interface SyncRegistration extends Jsonable { - - @JsonTypeName("register") - record Register(MavenCoordinates coordinates) implements SyncRegistration { - public Register { - } - } - - @JsonTypeName("register-batch") - record SyncBatch(ArtifactCoordinates artifact, List coordinates, akka.actor.typed.ActorRef replyTo) implements SyncRegistration { - - } - - @JsonTypeName("stop-batch") - enum Timeout implements SyncRegistration { - INSTANCE; - } - - @JsonTypeName("update-batch") - enum Refresh implements SyncRegistration { - INSTANCE; - } - - @JsonTypeName("duplicate-registration") - record AlreadyRegistered(MavenCoordinates coordinates) implements SyncRegistration { - } - - @JsonTypeName("failed-registration") - record RetryFailed(MavenCoordinates coordinates) implements SyncRegistration { - } - - @JsonTypeName("delay-retry-registration") - record DelayRegistration(MavenCoordinates coordinates, Duration duration) implements SyncRegistration { - - } - - @JsonTypeName("group-missing") - record GroupUnregistered(MavenCoordinates coordinates) implements SyncRegistration { - } - - @JsonTypeName("successful-registration") - record MarkRegistered(MavenCoordinates coordinates) implements SyncRegistration { - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionRegistrationState.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionRegistrationState.java deleted file mode 100644 index dd3ec5b3..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionRegistrationState.java +++ /dev/null @@ -1,336 +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.synchronizer.versionsync; - -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 com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = VersionRegistrationState.Empty.class, - name = "empty"), - @JsonSubTypes.Type(value = VersionRegistrationState.Registered.class, - name = "registered") -}) -public sealed interface VersionRegistrationState extends Jsonable { - - ArtifactCoordinates coordinates(); - - default boolean isActive() { - return false; - } - - VersionRegistrationState acceptBatch( - final ArtifactCoordinates artifact, final List coordinates - ); - - VersionRegistrationState acceptVersion(MavenCoordinates coordinates); - - default boolean hasVersion(MavenCoordinates coordinates) { - return false; - } - - default List getNextBatch() { - return List.empty(); - } - - VersionRegistrationState resolvedVersion(MavenCoordinates coordinates); - - default List getPending() { - return List.empty(); - } - - VersionRegistrationState startBatch(List batched); - - VersionRegistrationState failedVersion(MavenCoordinates coordinates); - - @JsonDeserialize - record Empty() implements VersionRegistrationState { - @JsonCreator - public Empty { - } - - @Override - public ArtifactCoordinates coordinates() { - return new ArtifactCoordinates("", ""); - } - - @Override - public VersionRegistrationState acceptBatch( - final ArtifactCoordinates artifact, - final List coordinates - ) { - return new Registered(artifact, coordinates.toMap(c -> c.version, (c) -> Registration.UNREGISTERED)); - } - - @Override - public VersionRegistrationState acceptVersion(final MavenCoordinates coordinates) { - return new Registered( - coordinates.asArtifactCoordinates(), - HashMap.of(coordinates.version, Registration.UNREGISTERED) - ); - } - - @Override - public VersionRegistrationState resolvedVersion(final MavenCoordinates coordinates) { - return new Registered( - coordinates.asArtifactCoordinates(), - HashMap.of(coordinates.version, Registration.REGISTERED) - ); - } - - @Override - public VersionRegistrationState startBatch( - final List batched - ) { - return new Active( - batched.head().asArtifactCoordinates(), - batched.toMap(c -> c.version, (c) -> Registration.UNREGISTERED), - batched - ); - } - - @Override - public VersionRegistrationState failedVersion(final MavenCoordinates coordinates) { - return new Registered(coordinates.asArtifactCoordinates(), HashMap.of(coordinates.version, Registration.UNREGISTERED)); - } - } - - @JsonDeserialize - enum Registration { - REGISTERED, - UNREGISTERED; - } - - @JsonDeserialize - record Registered( - ArtifactCoordinates coordinates, - Map versions - ) implements VersionRegistrationState { - @JsonCreator - public Registered { - } - - @Override - public boolean isActive() { - return false; - } - - @Override - public boolean hasVersion(final MavenCoordinates coordinates) { - return this.versions.containsKey(coordinates.version); - } - - @Override - public VersionRegistrationState acceptBatch( - final ArtifactCoordinates artifact, - final List coordinates - ) { - return new Registered( - this.coordinates, - this.versions.merge( - coordinates.toMap(c -> c.version, (c) -> Registration.UNREGISTERED), - (existing, registration) -> existing - ) - ); - } - - @Override - public VersionRegistrationState acceptVersion(final MavenCoordinates coordinates) { - if (this.versions.containsKey(coordinates.version)) { - return this; - } - return new Registered( - this.coordinates, - this.versions.put(coordinates.version, Registration.UNREGISTERED) - ); - } - - @Override - public List getNextBatch() { - return this.versions.filterValues(registration -> registration == Registration.UNREGISTERED) - .keySet() - .map(this.coordinates::version) - .toList() - .sorted() - .take(10); - } - - @Override - public VersionRegistrationState resolvedVersion(final MavenCoordinates coordinates) { - return new Registered( - this.coordinates, - this.versions.put(coordinates.version, Registration.REGISTERED) - ); - } - - @Override - public VersionRegistrationState startBatch( - final List batched - ) { - final var toBatch = batched.filter( - c -> this.versions.getOrElse(c.version, Registration.UNREGISTERED) == Registration.UNREGISTERED); - return new Active( - this.coordinates, - this.versions.merge( - toBatch.toMap(c -> c.version, (c) -> Registration.UNREGISTERED), - (existing, registration) -> existing - ), - toBatch - ); - } - - @Override - public VersionRegistrationState failedVersion(final MavenCoordinates coordinates) { - if (this.versions.containsKey(coordinates.version)) { - return this; - } - return new Registered( - this.coordinates, - this.versions.put(coordinates.version, Registration.UNREGISTERED) - ); - } - } - - record Active( - ArtifactCoordinates coordinates, - Map versions, - List queue - ) implements VersionRegistrationState { - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean hasVersion(final MavenCoordinates coordinates) { - return this.versions.containsKey(coordinates.version); - } - - @Override - public VersionRegistrationState resolvedVersion(final MavenCoordinates coordinates) { - final var pending = this.queue.remove(coordinates); - final var updated = this.versions.put(coordinates.version, Registration.REGISTERED); - if (pending.isEmpty()) { - return new Registered( - this.coordinates, - updated - ); - } - return new Active( - this.coordinates, - updated, - pending - ); - } - - @Override - public VersionRegistrationState acceptBatch( - final ArtifactCoordinates artifact, - final List coordinates - ) { - return new Active( - this.coordinates, - this.versions.merge( - coordinates.toMap(c -> c.version, (c) -> Registration.UNREGISTERED), - (existing, registration) -> existing - ), - this.queue - ); - } - - @Override - public VersionRegistrationState acceptVersion(final MavenCoordinates coordinates) { - if (this.versions.containsKey(coordinates.version)) { - return this; - } - return new Active( - this.coordinates, - this.versions.put(coordinates.version, Registration.UNREGISTERED), - this.queue - ); - } - - @Override - public List getPending() { - return this.queue; - } - - @Override - public VersionRegistrationState startBatch( - final List batched - ) { - if (!this.queue.isEmpty()) { - return this; - } - final var toBatch = batched.filter( - c -> this.versions.getOrElse(c.version, Registration.UNREGISTERED) == Registration.UNREGISTERED - ).appendAll(this.queue); - return new Active( - this.coordinates, - this.versions.merge( - toBatch.toMap(c -> c.version, (c) -> Registration.UNREGISTERED), - (existing, registration) -> existing - ), - toBatch - ); - } - - @Override - public VersionRegistrationState failedVersion(final MavenCoordinates coordinates) { - if (this.versions.containsKey(coordinates.version)) { - return this; - } - return new Active( - this.coordinates, - this.versions.put(coordinates.version, Registration.UNREGISTERED), - this.queue - ); - } - - @Override - public List getNextBatch() { - if (this.queue.isEmpty()) { - return this.versions.filterValues(registration -> registration == Registration.UNREGISTERED) - .keySet() - .map(this.coordinates::version) - .toList() - .sorted() - .take(10); - } - return this.queue; - } - } -} diff --git a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionSyncEvent.java b/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionSyncEvent.java deleted file mode 100644 index 67a3a7d4..00000000 --- a/version-synchronizer/src/main/java/org/spongepowered/synchronizer/versionsync/VersionSyncEvent.java +++ /dev/null @@ -1,77 +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.synchronizer.versionsync; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(VersionSyncEvent.RegisteredVersion.class), - @JsonSubTypes.Type(VersionSyncEvent.RegisteredBatch.class), - @JsonSubTypes.Type(VersionSyncEvent.StartedBatchRegistration.class), - @JsonSubTypes.Type(VersionSyncEvent.ResolvedVersion.class), - @JsonSubTypes.Type(VersionSyncEvent.FailedVersion.class) -}) -public sealed interface VersionSyncEvent extends AggregateEvent, Jsonable { - - AggregateEventShards INSTANCE = AggregateEventTag.sharded(VersionSyncEvent.class, 3); - - @Override - default AggregateEventTagger aggregateTag() { - return INSTANCE; - } - - @JsonTypeName("registered-version") - record RegisteredVersion(MavenCoordinates coordinates) implements VersionSyncEvent {} - - @JsonTypeName("registered-batch") - record RegisteredBatch(ArtifactCoordinates artifact, List coordinates) implements VersionSyncEvent { - } - - @JsonTypeName("started-batch") - record StartedBatchRegistration(List batched) implements VersionSyncEvent { - } - - @JsonTypeName("resolved-version") - record ResolvedVersion(MavenCoordinates coordinates) implements VersionSyncEvent { - } - - @JsonTypeName("failed-version") - record FailedVersion(MavenCoordinates coordinates) implements VersionSyncEvent { - } -} diff --git a/version-synchronizer/src/main/resources/META-INF/persistence.xml b/version-synchronizer/src/main/resources/META-INF/persistence.xml deleted file mode 100644 index df63fd1b..00000000 --- a/version-synchronizer/src/main/resources/META-INF/persistence.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - org.hibernate.jpa.HibernatePersistenceProvider - DefaultDS - true - - - - - - - diff --git a/version-synchronizer/src/main/resources/application.conf b/version-synchronizer/src/main/resources/application.conf deleted file mode 100644 index e0dd3d6d..00000000 --- a/version-synchronizer/src/main/resources/application.conf +++ /dev/null @@ -1,84 +0,0 @@ -play.modules.enabled += org.spongepowered.synchronizer.SynchronizerModule - -db.default { - driver = "org.postgresql.Driver" - url = "jdbc:postgresql://localhost:5432/default" - url = ${?POSTGRES_URL} - username = admin - username = ${?POSTGRES_USERNAME} - password = password - password = ${?POSTGRES_PASSWORD} -} -akka.cluster.roles += "commit-resolver" - -jdbc-defaults.slick.profile = "slick.jdbc.PostgresProfile$" - -akka.serialization.jackson { - jackson-modules += "io.vavr.jackson.datatype.VavrModule" -} -akka.persistence.snapshot-store { - snapshot-is-optional = true -} - -akka { - extensions = ${akka.extensions} [ - "org.spongepowered.synchronizer.assetsync.AssetSettingsExtension", - "org.spongepowered.synchronizer.resync.ResyncExtension", - "org.spongepowered.synchronizer.SynchronizationExtension", - "org.spongepowered.synchronizer.actor.ArtifactSyncExtension" - ] -} -asset-retrieval-dispatcher { - type = Dispatcher - executor = "thread-pool-executor" - thread-pool-executor { - fixed-pool-size = 16 - } - throughput = 1 -} -systemofadownload.synchronizer { - version-sync { - pool-size = 1 - interval = "300s" - delay = "60s" - } - reactive-sync { - pool-size = 1 - parallelism = 1 - time-out = "1h" - } - asset { - pool-size = 1 - parallelism = 1 - initial-backoff = "1m" - maximum-backoff = "600m" - backoff-factor = 10 - time-out = "1h" - } - timed-sync { - - } - worker { - assets { - repository = "https://repo.spongepowered.org" - timeout = "1h" - retry = 3 - files-to-index = ["jar", "pom"] - pool-size = 1 - } - resync { - repository = "https://repo.spongepowered.org/repository/maven-public/" - timeout = "1h" - retry = 1 - agent-name = "SystemOfADownload-Synchronizer" - } - version-registration { - parallelism = 1 - fan-out-parallelism = 1 - pool-size = 1 - time-out = "1h" - registration-time-out = "90000s" - } - } - -} diff --git a/version-synchronizer/src/main/resources/logback.xml b/version-synchronizer/src/main/resources/logback.xml deleted file mode 100644 index 8ca52f08..00000000 --- a/version-synchronizer/src/main/resources/logback.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - System.out - - %date{hh:MM:ss.SSS} [%level] [%thread] [%logger{5}/%marker] - %coloredLevel %msg%n - - - - - 8192 - true - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/version-synchronizer/src/main/resources/reference.conf b/version-synchronizer/src/main/resources/reference.conf deleted file mode 100644 index 15e6e946..00000000 --- a/version-synchronizer/src/main/resources/reference.conf +++ /dev/null @@ -1,25 +0,0 @@ -systemofadownload.commit-resolver { - cloner { - type = Dispatcher - executor = "thread-pool-executor" - thread-pool-executor { - fixed-pool-size = 2 - keep-alive-time = 60s - } - throughput = 1 - } - dispatcher { - type = Dispatcher - executor = "fork-join-executor" - fork-join-executor { - # Min number of threads to cap factor-based parallelism number to - parallelism-min = 2 - # Parallelism (threads) ... ceil(available processors * factor) - parallelism-factor = 2.0 - # Max number of threads to cap factor-based parallelism number to - parallelism-max = 10 - } - throughput = 2 - } - -} diff --git a/version-synchronizer/src/main/resources/soad.gitconfig b/version-synchronizer/src/main/resources/soad.gitconfig deleted file mode 100644 index e69de29b..00000000 diff --git a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java deleted file mode 100644 index 6f687f1e..00000000 --- a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitDetailsRegistrarTest.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.spongepowered.synchronizer.test.worker; - - -public class CommitDetailsRegistrarTest { - -} diff --git a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java deleted file mode 100644 index f431e2e8..00000000 --- a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/CommitResolutionManagerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.spongepowered.synchronizer.test.worker; - -import akka.Done; -import akka.actor.testkit.typed.javadsl.FishingOutcomes; -import akka.actor.testkit.typed.javadsl.TestKitJunitResource; -import io.vavr.collection.List; -import org.eclipse.jgit.util.SystemReader; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.synchronizer.actor.CommitDetailsRegistrar; -import org.spongepowered.synchronizer.gitmanaged.util.jgit.CommitResolutionManager; - -import java.io.File; -import java.net.URI; -import java.time.Duration; - -public class CommitResolutionManagerTest { - - private TestKitJunitResource testKit; - - @BeforeEach - public void setup() { - this.testKit = new TestKitJunitResource(); - testKit.system().log().info("Starting test"); - // Because jgit has to be told to ignore a lot of things about the system - // we need to proxy - final var dummyGitConfig = this.getClass().getClassLoader().getResource("dummy.gitconfig"); - Assertions.assertNotNull(dummyGitConfig, "dummy git config cannot be null"); - final var gitConfig = new File(dummyGitConfig.getFile()); - SystemReader.setInstance(new TestGitSystemReader(gitConfig)); - } - - @AfterEach - public void teardown() { - testKit.system().log().info("Finishing test"); - testKit.system().terminate(); - } - - @Test - public void testCommitResolver() { - final var teller = testKit.createTestProbe(CommitDetailsRegistrar.Command.class); - final var actor = testKit.spawn(CommitResolutionManager.resolveCommit(teller.ref())); - final var coords = new ArtifactCoordinates("com.example", "test").version("1.0"); - final var commit = "d838fee5d8e834ba9fd4d1c4fe0f8214d6dc90fc"; - final var url = "https://github.com/spongepowered/configurate.git"; - final var uri = List.of(URI.create(url)); - final var probe = testKit - .createTestProbe(); - final var replyTo = probe - .ref(); - actor.tell(new CommitResolutionManager.ResolveCommitDetails( - coords, commit, uri, replyTo - )); - // This teller is the intermediary registrar that should receive HandleVersionedCommitReport - // that ultimately tells the probe a Done. - teller.fishForMessage(Duration.ofSeconds(60), m -> { - ((CommitDetailsRegistrar.HandleVersionedCommitReport) m).replyTo().tell(Done.getInstance()); - return FishingOutcomes.complete(); - }); - - probe.fishForMessage(Duration.ofSeconds(60), m -> FishingOutcomes.complete()); - } - - @Test - public void verifyNonExistentRepo() { - final var teller = testKit.createTestProbe(CommitDetailsRegistrar.Command.class); - final var actor = testKit.spawn(CommitResolutionManager.resolveCommit(teller.ref())); - final var coords = new ArtifactCoordinates("com.example", "test").version("1.0"); - final var commit = "d838fee5d8e834ba9fd4d1c4fe0f8214d6dc90fc"; - final var url = "https://example.com/git/doesnt-exist.git"; - final var uri = List.of(URI.create(url)); - final var probe = testKit - .createTestProbe(); - final var replyTo = probe - .ref(); - actor.tell(new CommitResolutionManager.ResolveCommitDetails( - coords, commit, uri, replyTo - )); - probe.fishForMessage(Duration.ofSeconds(10), m -> FishingOutcomes.complete()); - } - - - @Test - public void verifyNonExistentCommit() { - final var teller = testKit.createTestProbe(CommitDetailsRegistrar.Command.class); - final var actor = testKit.spawn(CommitResolutionManager.resolveCommit(teller.ref())); - final var coords = new ArtifactCoordinates("com.example", "test").version("1.0"); - final var commit = "a830fee5d8e894ba9aa4a1a4fe0f8214d6dc90fc"; - final var url = "https://github.com/spongepowered/configurate.git"; - final var uri = List.of(URI.create(url)); - final var probe = testKit - .createTestProbe(); - final var replyTo = probe - .ref(); - actor.tell(new CommitResolutionManager.ResolveCommitDetails( - coords, commit, uri, replyTo - )); - probe.fishForMessage(Duration.ofSeconds(60), m -> FishingOutcomes.complete()); - } - - @Test - public void verifyCommitFromTwoRepositories() { - final var teller = testKit.createTestProbe(CommitDetailsRegistrar.Command.class); - final var actor = testKit.spawn(CommitResolutionManager.resolveCommit(teller.ref())); - final var coords = new ArtifactCoordinates("org.spongepowered", "spongevanilla").version("1.16.5-8.1.0-RC1184"); - final var commit = "6e443ec04ded4385d12c2e609360e81a770fbfcb"; - final var url = "https://github.com/spongepowered/spongevanilla.git"; - final var newUrl = "https://github.com/spongepowered/sponge.git"; - final var repos = List.of(URI.create(newUrl), URI.create(url)); - final var probe = testKit.createTestProbe(); - final var replyTo = probe.ref(); - actor.tell(new CommitResolutionManager.ResolveCommitDetails( - coords, commit, repos, replyTo - )); - probe.fishForMessage(Duration.ofMinutes(10), m -> FishingOutcomes.complete()); - } -} diff --git a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/TestGitSystemReader.java b/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/TestGitSystemReader.java deleted file mode 100644 index ff574c14..00000000 --- a/version-synchronizer/src/test/java/org/spongepowered/synchronizer/test/worker/TestGitSystemReader.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.spongepowered.synchronizer.test.worker; - -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.util.FS; -import org.eclipse.jgit.util.SystemReader; - -import java.io.File; - -public class TestGitSystemReader extends SystemReader { - private static final SystemReader proxy = SystemReader.getInstance(); - private final File userGitConfig; - - public TestGitSystemReader(File userGitConfig) { - super(); - this.userGitConfig = userGitConfig; - } - - @Override - public String getenv(String variable) { - return proxy.getenv(variable); - } - - @Override - public String getHostname() { - return proxy.getHostname(); - } - - @Override - public String getProperty(String key) { - return proxy.getProperty(key); - } - - @Override - public long getCurrentTime() { - return proxy.getCurrentTime(); - } - - @Override - public int getTimezone(long when) { - return proxy.getTimezone(when); - } - - @Override - public FileBasedConfig openUserConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, userGitConfig, fs); - } - - // Return an empty system configuration, based on example in SystemReader.Default#openSystemConfig - @Override - public FileBasedConfig openSystemConfig(Config parent, FS fs) { - return new FileBasedConfig(parent, this.userGitConfig, fs) { - @Override - public void load() { - } - - @Override - public boolean isOutdated() { - return false; - } - }; - } - - @Override - public FileBasedConfig openJGitConfig(final Config parent, final FS fs) { - return new FileBasedConfig(parent, this.userGitConfig, fs) { - @Override - public void load() { - } - - @Override - public boolean isOutdated() { - return false; - } - }; - } -} diff --git a/version-synchronizer/src/test/resources/dummy.gitconfig b/version-synchronizer/src/test/resources/dummy.gitconfig deleted file mode 100644 index e69de29b..00000000 diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/VersionsService.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/VersionsService.java deleted file mode 100644 index d1cbfdcb..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/VersionsService.java +++ /dev/null @@ -1,74 +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.versions.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.broker.Topic; -import com.lightbend.lagom.javadsl.api.transport.Method; -import org.spongepowered.downloads.versions.api.models.ArtifactUpdate; -import org.spongepowered.downloads.versions.api.models.CommitRegistration; -import org.spongepowered.downloads.versions.api.models.TagRegistration; -import org.spongepowered.downloads.versions.api.models.TagVersion; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; -import org.spongepowered.downloads.versions.api.models.VersionedArtifactUpdates; - -public interface VersionsService extends Service { - - ServiceCall registerArtifactCollection( - String groupId, String artifactId - ); - - ServiceCall registerArtifactTag(String groupId, String artifactId); - ServiceCall updateArtifactTag(String groupId, String artifactId); - - ServiceCall tagVersion(String groupId, String artifactId); - - ServiceCall registerCommit(String groupId, String artifactId, String version); - - Topic artifactUpdateTopic(); - - Topic versionedArtifactUpdatesTopic(); - - @Override - default Descriptor descriptor() { - return Service.named("versions") - .withCalls( - Service.restCall(Method.POST, "/versions/groups/:groupId/artifacts/:artifactId/versions", this::registerArtifactCollection), - Service.restCall(Method.POST, "/versions/groups/:groupId/artifacts/:artifactId/tags", this::registerArtifactTag), - Service.restCall(Method.PATCH, "/versions/groups/:groupId/artifacts/:artifactId/tags", this::updateArtifactTag), - Service.restCall(Method.POST, "/versions/groups/:groupId/artifacts/:artifactId/promotion", this::tagVersion), - Service.restCall(Method.PUT, "/versions/groups/:groupId/artifacts/:artifactId/versions/:version/commit", this::registerCommit) - ) - .withTopics( - Service.topic("artifact-update", this::artifactUpdateTopic), - Service.topic("versioned-artifact-updates", this::versionedArtifactUpdatesTopic) - ) - .withAutoAcl(true); - } - -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/ArtifactUpdate.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/ArtifactUpdate.java deleted file mode 100644 index f57902e0..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/ArtifactUpdate.java +++ /dev/null @@ -1,59 +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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagEntry; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = ArtifactUpdate.ArtifactVersionRegistered.class, name = "new-version"), - @JsonSubTypes.Type(value = ArtifactUpdate.TagRegistered.class, name = "tag-update"), -}) -public interface ArtifactUpdate { - - @JsonDeserialize - final record ArtifactVersionRegistered(@JsonProperty("coordinates") MavenCoordinates coordinates) implements ArtifactUpdate { - - @JsonCreator - public ArtifactVersionRegistered { - } - } - - @JsonDeserialize - record TagRegistered(@JsonProperty("coordinates") ArtifactCoordinates coordinates, @JsonProperty("tag") ArtifactTagEntry entry) implements ArtifactUpdate { - - @JsonCreator - public TagRegistered { - } - } - -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/CommitRegistration.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/CommitRegistration.java deleted file mode 100644 index 51410e28..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/CommitRegistration.java +++ /dev/null @@ -1,61 +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.versions.api.models; - -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 com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.net.URI; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) -@JsonSubTypes({ - @JsonSubTypes.Type(CommitRegistration.ResolvedCommit.class), - @JsonSubTypes.Type(CommitRegistration.FailedCommit.class) -}) -public sealed interface CommitRegistration extends Jsonable { - - record ResolvedCommit( - URI repo, - VersionedCommit versionedCommit, - MavenCoordinates coordinates - ) implements CommitRegistration { - @JsonCreator - public ResolvedCommit { - } - } - - record FailedCommit( - String commitSha, URI repo - ) implements CommitRegistration { - @JsonCreator - public FailedCommit { - } - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagRegistration.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagRegistration.java deleted file mode 100644 index fb6dfbc0..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagRegistration.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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagEntry; - -public interface TagRegistration { - - @JsonDeserialize - final record Register(@JsonProperty("tag") ArtifactTagEntry entry) {} - - @JsonDeserialize - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.TagAlreadyRegistered.class, name = "AlreadyRegistered"), - @JsonSubTypes.Type(value = Response.TagSuccessfullyRegistered.class, name = "Success") - }) - interface Response extends Jsonable { - - final record TagAlreadyRegistered(@JsonProperty String name) implements TagRegistration.Response {} - - @JsonSerialize - final record TagSuccessfullyRegistered() implements TagRegistration.Response {} - - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagVersion.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagVersion.java deleted file mode 100644 index 544dbe72..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/TagVersion.java +++ /dev/null @@ -1,76 +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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; - -public interface TagVersion { - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Request.SetRecommendationRegex.class, name = "recommendation") - }) - interface Request { - - @JsonDeserialize - final record SetRecommendationRegex( - String regex, - List valid, - List invalid, - boolean enableManualMarking - ) implements Request { - - @JsonCreator - public SetRecommendationRegex( - @JsonProperty(required = true) final String regex, - @JsonProperty(required = true) final List valid, - @JsonProperty(required = true) final List invalid, - @JsonProperty(required = true) final boolean enableManualMarking - ) { - this.regex = regex; - this.enableManualMarking = enableManualMarking; - this.valid = valid; - this.invalid = invalid; - } - } - } - - @JsonSubTypes({ - @JsonSubTypes.Type(value = TagVersion.Response.TagSuccessfullyRegistered.class, name = "Success") - }) - interface Response extends Jsonable { - - @JsonSerialize - final record TagSuccessfullyRegistered() implements TagVersion.Response {} - - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionRegistration.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionRegistration.java deleted file mode 100644 index 240fbf19..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionRegistration.java +++ /dev/null @@ -1,109 +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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCollection; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -public final class VersionRegistration { - - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Register.Collection.class, - name = "Collection"), - @JsonSubTypes.Type(value = Register.Version.class, - name = "Version"), - }) - public interface Register { - - @JsonDeserialize - final record Collection( - @JsonProperty ArtifactCollection collection - ) implements Register { - - @JsonCreator - public Collection { - } - - } - - @JsonDeserialize - record Version(@JsonProperty MavenCoordinates coordinates) - implements Register { - @JsonCreator - public Version { - } - - } - - - } - - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, - property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = Response.GroupMissing.class, - name = "GroupMissing"), - @JsonSubTypes.Type(value = Response.ArtifactAlreadyRegistered.class, - name = "AlreadyRegistered"), - @JsonSubTypes.Type(value = Response.RegisteredArtifact.class, - name = "Registered"), - }) - public interface Response extends Jsonable { - - @JsonDeserialize - final record ArtifactAlreadyRegistered( - @JsonProperty(required = true) MavenCoordinates coordinates - ) implements Response { - - @JsonCreator - public ArtifactAlreadyRegistered { - } - } - - @JsonDeserialize - final record RegisteredArtifact( - @JsonProperty(required = true) MavenCoordinates mavenCoordinates - ) implements Response { - - @JsonCreator - public RegisteredArtifact { - } - } - - @JsonDeserialize - final record GroupMissing(@JsonProperty(required = true) String groupId) implements Response { - - } - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedArtifactUpdates.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedArtifactUpdates.java deleted file mode 100644 index 4c8042c5..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedArtifactUpdates.java +++ /dev/null @@ -1,62 +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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.net.URI; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = VersionedArtifactUpdates.GitCommitDetailsAssociated.class), - @JsonSubTypes.Type(value = VersionedArtifactUpdates.CommitExtracted.class), -}) -public interface VersionedArtifactUpdates extends Jsonable { - - @JsonTypeName("commit-extracted") - final record CommitExtracted( - MavenCoordinates coordinates, - List gitRepositories, - String commit - ) implements VersionedArtifactUpdates { - - @JsonCreator - public CommitExtracted { - } - } - @JsonTypeName("commit-associated") - final record GitCommitDetailsAssociated( - MavenCoordinates coordinates, - URI repo, VersionedCommit commit - ) implements VersionedArtifactUpdates { - - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedChangelog.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedChangelog.java deleted file mode 100644 index c7a8b2e4..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedChangelog.java +++ /dev/null @@ -1,65 +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.versions.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -import java.net.URI; - -@JsonDeserialize -public final record VersionedChangelog( - List commits, - @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean processing -) { - - @JsonCreator - public VersionedChangelog { - } - - @JsonDeserialize - public final record IndexedCommit( - VersionedCommit commit, - List submoduleCommits - ) { - @JsonCreator - public IndexedCommit { - } - } - - @JsonDeserialize - public final record Submodule( - String name, - URI gitRepository, - List commits - ) { - @JsonCreator - public Submodule { - } - } - -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedCommit.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedCommit.java deleted file mode 100644 index e73d9bc0..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/VersionedCommit.java +++ /dev/null @@ -1,70 +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.versions.api.models; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.net.URI; -import java.time.ZonedDateTime; -import java.util.Optional; - -@JsonDeserialize -public record VersionedCommit( - String message, - String body, - String sha, - Author author, - Commiter commiter, - Optional link, - ZonedDateTime commitDate -) { - - @JsonCreator - public VersionedCommit { - } - - @JsonDeserialize - public record Author( - String name, - String email - ) { - @JsonCreator - public Author { - } - } - - @JsonDeserialize - public record Commiter( - String name, - String email - ) { - @JsonCreator - public Commiter { - } - } -} - diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagEntry.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagEntry.java deleted file mode 100644 index 266ccd63..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagEntry.java +++ /dev/null @@ -1,58 +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.versions.api.models.tags; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.control.Try; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.util.regex.Pattern; - -@JsonDeserialize -public record ArtifactTagEntry( - @JsonProperty(required = true) String name, - @JsonProperty(required = true) int matchingGroup, - @JsonProperty(required = true) String regex -) { - - @JsonCreator - public ArtifactTagEntry { - } - - public VersionTagValue generateValue(MavenCoordinates coordinates) { - final var expectedGroup = this.matchingGroup(); - final var matcher = Pattern.compile(this.regex()).matcher(coordinates.version); - final String value; - if (matcher.find()) { - value = Try.of(() -> matcher.group(expectedGroup)) - .getOrElse(""); - } else { - value = ""; - } - return new VersionTagValue(coordinates, this, value); - } -} diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagValue.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagValue.java deleted file mode 100644 index 01ec9590..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/ArtifactTagValue.java +++ /dev/null @@ -1,43 +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.versions.api.models.tags; - -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -public final record ArtifactTagValue( - MavenCoordinates coordinates, - Map tagValues, - boolean recommended -) { - public ArtifactTagValue promote(boolean promoted) { - return new ArtifactTagValue( - this.coordinates, - this.tagValues, - promoted - ); - } -} - diff --git a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/VersionTagValue.java b/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/VersionTagValue.java deleted file mode 100644 index a5d80b5e..00000000 --- a/versions-api/src/main/java/org/spongepowered/downloads/versions/api/models/tags/VersionTagValue.java +++ /dev/null @@ -1,35 +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.versions.api.models.tags; - -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -public final record VersionTagValue( - MavenCoordinates coordinates, - ArtifactTagEntry tag, - String tagValue -) { - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsModule.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsModule.java deleted file mode 100644 index 773c2ed5..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsModule.java +++ /dev/null @@ -1,75 +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.versions.server; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; -import org.pac4j.core.config.Config; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.auth.SOADAuth; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.server.readside.AssetReadsidePersistence; -import org.spongepowered.downloads.versions.server.readside.VersionReadSidePersistence; -import org.spongepowered.downloads.versions.worker.readside.CommitProcessor; -import play.Environment; -import play.libs.akka.AkkaGuiceSupport; - -import javax.inject.Inject; - -public class VersionsModule extends AbstractModule implements ServiceGuiceSupport, AkkaGuiceSupport { - - private final AuthUtils auth; - - @SuppressWarnings("unused") // These parameters must match for Play's Guice handling to work. - @Inject - public VersionsModule(final Environment environment, final com.typesafe.config.Config config) { - this.auth = AuthUtils.configure(config); - } - - @Override - protected void configure() { - this.bindService(VersionsService.class, VersionsServiceImpl.class); - this.bindClient(ArtifactService.class); - - this.bind(VersionReadSidePersistence.class).asEagerSingleton(); - this.bind(AssetReadsidePersistence.class).asEagerSingleton(); - this.bind(CommitProcessor.class).asEagerSingleton(); - - } - - @Provides - @SOADAuth - protected Config configProvider() { - return this.auth.config(); - } - - @Provides - protected AuthUtils authProvider() { - return this.auth; - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsServiceImpl.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsServiceImpl.java deleted file mode 100644 index f0ffaddf..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/VersionsServiceImpl.java +++ /dev/null @@ -1,370 +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.versions.server; - -import akka.Done; -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.japi.Pair; -import akka.stream.javadsl.Flow; -import com.google.inject.Inject; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.broker.Topic; -import com.lightbend.lagom.javadsl.api.transport.BadRequest; -import com.lightbend.lagom.javadsl.api.transport.NotFound; -import com.lightbend.lagom.javadsl.broker.TopicProducer; -import com.lightbend.lagom.javadsl.persistence.Offset; -import com.lightbend.lagom.javadsl.persistence.PersistentEntityRegistry; -import com.lightbend.lagom.javadsl.server.ServerServiceCall; -import io.vavr.control.Try; -import org.pac4j.core.config.Config; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.artifact.api.event.GroupUpdate; -import org.spongepowered.downloads.auth.AuthenticatedInternalService; -import org.spongepowered.downloads.auth.SOADAuth; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import org.spongepowered.downloads.versions.api.models.ArtifactUpdate; -import org.spongepowered.downloads.versions.api.models.CommitRegistration; -import org.spongepowered.downloads.versions.api.models.TagRegistration; -import org.spongepowered.downloads.versions.api.models.TagVersion; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; -import org.spongepowered.downloads.versions.api.models.VersionedArtifactUpdates; -import org.spongepowered.downloads.versions.server.domain.ACCommand; -import org.spongepowered.downloads.versions.server.domain.ACEvent; -import org.spongepowered.downloads.versions.server.domain.InvalidRequest; -import org.spongepowered.downloads.versions.server.domain.VersionedArtifactAggregate; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.ArtifactEvent; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactCommand; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactEntity; - -import java.net.URI; -import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class VersionsServiceImpl implements VersionsService, - AuthenticatedInternalService { - private final PersistentEntityRegistry persistentEntityRegistry; - private final Config securityConfig; - private final ClusterSharding clusterSharding; - private final Duration streamTimeout = Duration.ofSeconds(30); - private final AuthUtils auth; - - public static final Pattern VALID_COORDINATE_PORTION = Pattern.compile("^[\\w.-]+$"); - - @Inject - public VersionsServiceImpl( - final ClusterSharding clusterSharding, - final ArtifactService artifactService, - final PersistentEntityRegistry persistentEntityRegistry, - @SOADAuth final Config securityConfig, - final AuthUtils auth - ) { - this.clusterSharding = clusterSharding; - this.persistentEntityRegistry = persistentEntityRegistry; - this.securityConfig = securityConfig; - this.auth = auth; - - this.clusterSharding.init( - Entity.of( - VersionedArtifactAggregate.ENTITY_TYPE_KEY, - VersionedArtifactAggregate::create - ) - ); - - artifactService.groupTopic() - .subscribe() - .atLeastOnce(Flow.create().map(this::processGroupEvent)); - - } - - private Done processGroupEvent(GroupUpdate a) { - if (!(a instanceof GroupUpdate.ArtifactRegistered g)) { - return Done.done(); - } - final var coordinates = g.coordinates(); - return this.getCollection(coordinates) - .ask( - replyTo -> new ACCommand.RegisterArtifact(coordinates, replyTo), - this.streamTimeout - ) - .thenApply(notUsed -> Done.done()) - .toCompletableFuture() - .join(); - } - - @Override - public Config getSecurityConfig() { - return this.securityConfig; - } - - @Override - public ServerServiceCall registerArtifactCollection( - final String groupId, - final String artifactId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> registration -> { - final var coordinates = parseCoordinates(groupId, artifactId); - if (registration instanceof VersionRegistration.Register.Version v) { - return this.getCollection(coordinates) - .ask( - replyTo -> new ACCommand.RegisterVersion(v.coordinates(), replyTo), - this.streamTimeout - ).thenApply(response -> { - if (response instanceof InvalidRequest) { - throw new NotFound("unknown artifact or group"); - } - return response; - }) - .thenCompose(r -> this.clusterSharding.entityRefFor( - VersionedArtifactEntity.ENTITY_TYPE_KEY, - v.coordinates().asStandardCoordinates() - ) - .ask( - replyTo -> new VersionedArtifactCommand.Register(v.coordinates(), replyTo), - this.streamTimeout - ) - .thenApply(notUsed -> r)); - } - if (registration instanceof VersionRegistration.Register.Collection c) { - return this.clusterSharding.entityRefFor( - VersionedArtifactEntity.ENTITY_TYPE_KEY, c.collection().coordinates().asStandardCoordinates()) - .ask( - replyTo -> new VersionedArtifactCommand.RegisterAssets( - c.collection().coordinates(), c.collection(), replyTo), this.streamTimeout) - .thenApply(response -> { - if (response instanceof InvalidRequest) { - throw new NotFound("unknown artifact or group"); - } - return response; - }); - } - throw new BadRequest("unknown registration request"); - }); - } - - @Override - public ServiceCall registerArtifactTag( - final String groupId, - final String artifactId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> registration -> { - final var coordinates = parseCoordinates(groupId, artifactId); - return this.getCollection(coordinates) - .ask( - replyTo -> new ACCommand.RegisterArtifactTag(registration.entry(), replyTo), this.streamTimeout) - .thenApply(response -> { - if (response instanceof InvalidRequest) { - throw new NotFound("unknown artifact or group"); - } - return response; - }); - }); - } - - @Override - public ServiceCall updateArtifactTag( - final String groupId, - final String artifactId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> registration -> { - final var coordinates = parseCoordinates(groupId, artifactId); - return this.getCollection(coordinates) - .ask( - replyTo -> new ACCommand.UpdateArtifactTag(registration.entry(), replyTo), this.streamTimeout) - .thenApply(response -> { - if (response instanceof InvalidRequest) { - throw new NotFound("unknown artifact or group"); - } - return response; - }); - }); - } - - @Override - public ServiceCall tagVersion( - final String groupId, - final String artifactId - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> request -> { - final var coordinates = parseCoordinates(groupId, artifactId); - if (!(request instanceof TagVersion.Request.SetRecommendationRegex s)) { - throw new BadRequest("unknown request"); - } - final var regex = Try.of(() -> Pattern.compile(s.regex())); - final var validFailures = s.valid() - .filter(valid -> regex.map(pattern -> pattern.matcher(valid)) - .mapTry(Matcher::find) - .map(b -> !b) - .getOrElse(true) // If exception, keep the version as failed - ); - final var invalidSuccesses = s.invalid() - .filter(invalid -> regex - .map(pattern -> pattern.matcher(invalid)) - .mapTry(Matcher::find) - .getOrElse(true) - ); - if (!validFailures.isEmpty()) { - throw new BadRequest("expected valid versions did not match regex: " + validFailures); - } - if (!invalidSuccesses.isEmpty()) { - throw new BadRequest("expected invalid versions matched regex successfully:" + invalidSuccesses); - } - return this.getCollection(coordinates) - .ask( - replyTo -> new ACCommand.RegisterPromotion(s.regex(), replyTo, s.enableManualMarking()), - this.streamTimeout - ) - .thenApply(response -> { - if (response instanceof InvalidRequest) { - throw new NotFound("unknown artifact or group"); - } - return response; - }); - }); - } - - @Override - public ServiceCall registerCommit( - final String groupId, final String artifactId, final String version - ) { - return this.authorize(AuthUtils.Types.JWT, AuthUtils.Roles.ADMIN, profile -> request -> { - final var coordinates = parseCoordinates(groupId, artifactId); - if (!VALID_COORDINATE_PORTION.matcher(version).matches()) { - throw new BadRequest("Invalid version: " + version); - } - final var mavenCoordinates = coordinates.version(version); - if (request instanceof CommitRegistration.ResolvedCommit rc) { - return this.clusterSharding.entityRefFor( - VersionedArtifactEntity.ENTITY_TYPE_KEY, - mavenCoordinates.asStandardCoordinates() - ) - .ask( - replyTo -> new VersionedArtifactCommand.RegisterResolvedCommit( - rc.versionedCommit(), - rc.repo(), - replyTo - ), - Duration.ofSeconds(30) - ) - .thenApply(done -> NotUsed.notUsed()); - } else if (request instanceof CommitRegistration.FailedCommit uc) { - return this.clusterSharding.entityRefFor( - VersionedArtifactEntity.ENTITY_TYPE_KEY, - mavenCoordinates.asStandardCoordinates() - ) - .ask( - replyTo -> new VersionedArtifactCommand.RegisterFailedCommit( - uc.commitSha(), - uc.repo(), - replyTo - ), - Duration.ofSeconds(30) - ) - .thenApply(done -> NotUsed.notUsed()); - } - - return CompletableFuture.completedStage(NotUsed.notUsed()); - }); - } - - private static List> convertEvent(Pair pair) { - final ACEvent event = pair.first(); - final ArtifactUpdate update; - if (event instanceof ACEvent.ArtifactVersionRegistered r) { - update = new ArtifactUpdate.ArtifactVersionRegistered(r.version()); - } else if (event instanceof ACEvent.ArtifactTagRegistered r) { - update = new ArtifactUpdate.TagRegistered(r.coordinates(), r.entry()); - } else { - return Collections.emptyList(); - } - return List.of(Pair.apply(update, pair.second())); - } - - @Override - public Topic artifactUpdateTopic() { - return TopicProducer.taggedStreamWithOffset( - ACEvent.INSTANCE.allTags(), - (aggregateTag, fromOffset) -> this.persistentEntityRegistry - .eventStream(aggregateTag, fromOffset) - .mapConcat(VersionsServiceImpl::convertEvent) - ); - } - - @Override - public Topic versionedArtifactUpdatesTopic() { - return TopicProducer.taggedStreamWithOffset( - ArtifactEvent.INSTANCE.allTags(), - (aggregateTag, fromOffset) -> this.persistentEntityRegistry - .eventStream(aggregateTag, fromOffset) - .mapConcat(VersionsServiceImpl::convertGitEvents) - ); - } - - private static List> convertGitEvents(Pair pair) { - final ArtifactEvent event = pair.first(); - final VersionedArtifactUpdates update; - if (event instanceof ArtifactEvent.CommitAssociated r) { - update = new VersionedArtifactUpdates.CommitExtracted( - r.coordinates(), r.repos().map(URI::create), r.commitSha()); - } else if (event instanceof ArtifactEvent.CommitResolved r) { - update = new VersionedArtifactUpdates.GitCommitDetailsAssociated( - r.coordinates(), r.repo(), r.versionedCommit()); - } else { - return Collections.emptyList(); - } - return List.of(Pair.apply(update, pair.second())); - } - - private EntityRef getCollection(final ArtifactCoordinates coordinates) { - return this.clusterSharding.entityRefFor( - VersionedArtifactAggregate.ENTITY_TYPE_KEY, coordinates.asMavenString()); - } - - private static ArtifactCoordinates parseCoordinates(final String groupID, final String artifactID) { - final String sanitizedGroupId = groupID.toLowerCase(Locale.ROOT); - if (!VALID_COORDINATE_PORTION.matcher(sanitizedGroupId).matches()) { - throw new BadRequest("Invalid groupId: " + groupID); - } - final String sanitizedArtifactId = artifactID.toLowerCase(Locale.ROOT); - if (!VALID_COORDINATE_PORTION.matcher(sanitizedArtifactId).matches()) { - throw new BadRequest("Invalid artifactId: " + artifactID); - } - return new ArtifactCoordinates(sanitizedGroupId, sanitizedArtifactId); - } - - @Override - public AuthUtils auth() { - return this.auth; - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACCommand.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACCommand.java deleted file mode 100644 index 0335c164..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACCommand.java +++ /dev/null @@ -1,82 +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.versions.server.domain; - -import akka.NotUsed; -import akka.actor.typed.ActorRef; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.TagRegistration; -import org.spongepowered.downloads.versions.api.models.TagVersion; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagEntry; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = ACCommand.RegisterArtifact.class, name = "register-artifact"), - @JsonSubTypes.Type(value = ACCommand.RegisterVersion.class, name = "register-version"), - @JsonSubTypes.Type(value = ACCommand.RegisterArtifactTag.class, name = "register-tag"), - @JsonSubTypes.Type(value = ACCommand.UpdateArtifactTag.class, name = "update-tag"), - @JsonSubTypes.Type(value = ACCommand.RegisterPromotion.class, name = "register-promotion"), -}) -public sealed interface ACCommand extends Jsonable{ - - record RegisterArtifact( - ArtifactCoordinates coordinates, - ActorRef replyTo - ) implements ACCommand { - } - - record RegisterVersion( - MavenCoordinates coordinates, - ActorRef replyTo - ) implements ACCommand { - } - - record RegisterArtifactTag( - ArtifactTagEntry entry, - ActorRef replyTo - ) implements ACCommand { - } - - record UpdateArtifactTag( - ArtifactTagEntry entry, - ActorRef replyTo - ) implements ACCommand { - } - - record RegisterPromotion( - String regex, - ActorRef replyTo, - boolean enableManualMarking - ) implements ACCommand { - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACEvent.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACEvent.java deleted file mode 100644 index 8897e94f..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/ACEvent.java +++ /dev/null @@ -1,134 +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.versions.server.domain; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagEntry; - -import java.io.Serial; -import java.util.Objects; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME) -@JsonSubTypes({ - @JsonSubTypes.Type( - value = ACEvent.ArtifactTagRegistered.class, - name = "tag-registered" - ), - @JsonSubTypes.Type( - value = ACEvent.ArtifactCoordinatesUpdated.class, - name = "updated-coordinates" - ), - @JsonSubTypes.Type( - value = ACEvent.ArtifactVersionRegistered.class, - name = "version-registered" - ), - @JsonSubTypes.Type( - value = ACEvent.PromotionSettingModified.class, - name = "promotion-settings-modified" - ), - @JsonSubTypes.Type( - value = ACEvent.ArtifactVersionsResorted.class, - name = "versions-resorted" - ) -}) -public interface ACEvent extends AggregateEvent, Jsonable { - AggregateEventShards INSTANCE = AggregateEventTag.sharded(ACEvent.class, 10); - - @Override - default AggregateEventTagger aggregateTag() { - return INSTANCE; - } - - record ArtifactCoordinatesUpdated(ArtifactCoordinates coordinates) implements ACEvent { - - @JsonCreator - public ArtifactCoordinatesUpdated { - } - - } - - record ArtifactVersionRegistered( - MavenCoordinates version, - int sorting - ) implements ACEvent { - @Serial private static final long serialVersionUID = 0L; - - @JsonCreator - public ArtifactVersionRegistered { - } - - @Override - public boolean equals(final Object obj) { - if (obj == this) { - return true; - } - if (obj == null || obj.getClass() != this.getClass()) { - return false; - } - final var that = (ArtifactVersionRegistered) obj; - return Objects.equals(this.version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(this.version); - } - - @Override - public String toString() { - return "ArtifactVersionRegistered[" + - "version=" + this.version + ", "; - } - } - - @JsonDeserialize - record ArtifactTagRegistered(ArtifactCoordinates coordinates, @JsonProperty("entry") ArtifactTagEntry entry) - implements ACEvent { - - } - - @JsonDeserialize - record PromotionSettingModified(ArtifactCoordinates coordinates, String regex, boolean enableManualPromotion) - implements ACEvent { - } - - @JsonDeserialize - record ArtifactVersionsResorted( - ArtifactCoordinates coordinates, Map versionordering - ) implements ACEvent{ - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/InvalidRequest.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/InvalidRequest.java deleted file mode 100644 index fa444b46..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/InvalidRequest.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.versions.server.domain; - -import org.spongepowered.downloads.versions.api.models.TagRegistration; -import org.spongepowered.downloads.versions.api.models.TagVersion; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; - -/** - * An invalid request to return to the asker in the service implementation to - * signify the current state is literally invalid to perform the specified - * action. - */ -public record InvalidRequest() - implements TagRegistration.Response, - TagVersion.Response, - VersionRegistration.Response { -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/State.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/State.java deleted file mode 100644 index d69a07fd..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/State.java +++ /dev/null @@ -1,223 +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.versions.server.domain; - -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.CompressedJsonable; -import io.vavr.Tuple2; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import io.vavr.collection.SortedMap; -import io.vavr.collection.TreeMap; -import org.apache.maven.artifact.versioning.ComparableVersion; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagEntry; -import org.spongepowered.downloads.versions.api.models.tags.ArtifactTagValue; - -import java.util.Collections; -import java.util.Comparator; -import java.util.Locale; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Pattern; - -public interface State { - boolean isRegistered(); - - static Empty empty() { - return new Empty(); - } - - final record Empty() implements State { - @Override - public boolean isRegistered() { - return false; - } - - public ACState register(ACEvent.ArtifactCoordinatesUpdated event) { - return new ACState(event.coordinates()); - } - } - - @JsonDeserialize - final record ACState( - ArtifactCoordinates coordinates, - SortedMap collection, - Map> versionedArtifacts, - boolean unregistered, - Map tags, - String promotionRegex, - boolean manualPromotionAllowed - ) implements CompressedJsonable, State { - - ACState(ArtifactCoordinates coordinates) { - this(coordinates, TreeMap.empty(), HashMap.empty(), false, HashMap.empty(), "", false); - } - - public boolean isRegistered() { - return !this.unregistered; - } - - public ACState withVersion(String version) { - final var versionMap = this.collection - .computeIfAbsent(version, convertArtifactVersionToTagValues(this, this.tags)) - ._2 - .toSortedMap(Comparator.comparing(ComparableVersion::new).reversed(), Tuple2::_1, Tuple2::_2); - return new ACState( - this.coordinates, - versionMap, - this.versionedArtifacts, - this.unregistered, - this.tags, - "", - false - ); - } - - public ACState withTag(ArtifactTagEntry entry) { - final var tagMap = this.tags().put(entry.name().toLowerCase(Locale.ROOT), entry); - final var versionedTags = this.collection - .replaceAll((version, values) -> convertArtifactVersionToTagValues(this, tagMap).apply(version)); - return new ACState( - this.coordinates, - versionedTags, - this.versionedArtifacts, - this.unregistered, - tagMap, - "", - false - ); - } - - private Function convertArtifactVersionToTagValues( - ACState state, Map tagMap - ) { - return version -> { - final var mavenCoordinates = state.coordinates.version(version); - final Map tagValues = tagMap.mapValues( - tag -> tag.generateValue(mavenCoordinates).tagValue()); - return new ArtifactTagValue(state.coordinates().version(version), tagValues, false); - }; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ACState acState = (ACState) o; - return Objects.equals(coordinates, acState.coordinates) && Objects.equals( - collection, acState.collection); - } - - @Override - public int hashCode() { - return Objects.hash(coordinates, collection); - } - - @Override - public String toString() { - return new StringJoiner( - ", ", ACState.class.getSimpleName() + "[", "]") - .add("coordinates=" + coordinates) - .add("collection=" + collection) - .toString(); - } - - public ACState withPromotionDetails(String regex, boolean enableManualPromotion) { - final var pattern = Pattern.compile(regex); - final var versionedTags = this.collection - .replaceAll((version, value) -> value.promote(pattern.matcher(version).find())) - .toSortedMap(Comparator.comparing(ComparableVersion::new).reversed(), Tuple2::_1, Tuple2::_2); - return new ACState( - this.coordinates, - versionedTags, - this.versionedArtifacts, - false, - this.tags, - regex, - enableManualPromotion - ); - } - - public ACState withAddedArtifacts(MavenCoordinates coordinates, List newArtifacts) { - final var existing = this.versionedArtifacts.get(coordinates.version) - .getOrElse(List::empty); - final var existingArtifactsByClassifier = existing.toMap(a -> a.classifier().orElse(""), Function.identity()); - final var newArtifactList = existing.appendAll( - newArtifacts.filter(Predicate.not(artifact -> existingArtifactsByClassifier.containsKey(artifact.classifier().orElse(""))))); - final var versionedArtifacts = this.versionedArtifacts.put(coordinates.version, newArtifactList); - return new ACState( - this.coordinates, - this.collection, - versionedArtifacts, - false, - this.tags, - this.promotionRegex, - this.manualPromotionAllowed - ); - } - - public java.util.List addVersion(MavenCoordinates coordinates) { - final var versions = this.collection - .keySet() - .toSortedSet(Comparator.comparing(ComparableVersion::new)); - final var newVersions = versions.add(coordinates.version); - final var newIndex = newVersions - .toList() - .indexOf(coordinates.version); - final var versionRegistered = new ACEvent.ArtifactVersionRegistered(coordinates, newIndex); - final var events = List.empty(); - if (newIndex >= versions.size()) { - return events.append(versionRegistered).toJavaList(); - } - if (versions.size() == newVersions.size()) { - return Collections.emptyList(); - } - // Figure out how many versions are being resorted - final var sortedVersions = newVersions.toSortedSet(Comparator.comparing(ComparableVersion::new)); - final java.util.Map versionsByIndex = new java.util.HashMap<>(); - versions.forEachWithIndex(versionsByIndex::put); - final java.util.Map updatedVersionsIndecies = new java.util.HashMap<>(); - sortedVersions.forEachWithIndex(updatedVersionsIndecies::put); - versionsByIndex.forEach((version, oldIndex) -> { - if (Objects.equals(updatedVersionsIndecies.get(version), oldIndex)) { - updatedVersionsIndecies.remove(version); - } - }); - final var trimmedIndecies = HashMap.ofAll(updatedVersionsIndecies); - final var versionMoved = new ACEvent.ArtifactVersionsResorted(this.coordinates, trimmedIndecies); - return events.append(versionRegistered).append(versionMoved).toJavaList(); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactAggregate.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactAggregate.java deleted file mode 100644 index e475d30e..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactAggregate.java +++ /dev/null @@ -1,189 +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.versions.server.domain; - -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 akka.persistence.typed.javadsl.ReplyEffect; -import akka.persistence.typed.javadsl.RetentionCriteria; -import com.lightbend.lagom.javadsl.persistence.AkkaTaggerAdapter; -import org.spongepowered.downloads.versions.api.models.TagRegistration; -import org.spongepowered.downloads.versions.api.models.TagVersion; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; - -import java.util.Locale; -import java.util.Set; -import java.util.function.Function; - -public final class VersionedArtifactAggregate - extends EventSourcedBehaviorWithEnforcedReplies { - - public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create(ACCommand.class, "VersionedArtifact"); - private final Function> tagger; - private final ActorContext ctx; - - public static Behavior create(final EntityContext context) { - return Behaviors.setup(ctx -> new VersionedArtifactAggregate(context, ctx)); - } - - private VersionedArtifactAggregate( - final EntityContext context, - final ActorContext ctx - ) { - 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 - )); - this.tagger = AkkaTaggerAdapter.fromLagom(context, ACEvent.INSTANCE); - this.ctx = ctx; - } - - @Override - public State emptyState() { - return State.empty(); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - builder.forStateType(State.Empty.class) - .onEvent(ACEvent.ArtifactCoordinatesUpdated.class, State.Empty::register); - builder.forStateType(State.ACState.class) - .onEvent(ACEvent.ArtifactTagRegistered.class, (state1, event1) -> state1.withTag(event1.entry())) - .onEvent( - ACEvent.ArtifactVersionRegistered.class, - (state2, event2) -> state2.withVersion(event2.version().version) - ) - .onEvent( - ACEvent.PromotionSettingModified.class, - (state, event) -> state.withPromotionDetails(event.regex(), event.enableManualPromotion()) - ) - .onEvent(ACEvent.ArtifactVersionsResorted.class, (state, event) -> state) - ; - return builder.build(); - } - - @Override - public Set tagsFor(final ACEvent acEvent) { - return this.tagger.apply(acEvent); - } - - @Override - public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(10, 2); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - builder.forStateType(State.Empty.class) - .onCommand(ACCommand.RegisterArtifact.class, this::handleRegisterArtifact) - .onCommand( - ACCommand.RegisterArtifactTag.class, (cmd) -> this.Effect().reply(cmd.replyTo(), new InvalidRequest())) - .onCommand( - ACCommand.RegisterVersion.class, (cmd) -> this.Effect().reply(cmd.replyTo(), new InvalidRequest())) - .onCommand( - ACCommand.RegisterArtifactTag.class, (cmd) -> this.Effect().reply(cmd.replyTo(), new InvalidRequest())) - .onCommand( - ACCommand.UpdateArtifactTag.class, (cmd) -> this.Effect().reply(cmd.replyTo(), new InvalidRequest())) - .onCommand( - ACCommand.RegisterPromotion.class, (cmd) -> this.Effect().reply(cmd.replyTo(), new InvalidRequest())) - ; - builder.forStateType(State.ACState.class) - .onCommand(ACCommand.RegisterArtifact.class, (cmd) -> this.Effect().reply(cmd.replyTo(), NotUsed.notUsed())) - .onCommand(ACCommand.RegisterVersion.class, this::handleRegisterVersion) - .onCommand(ACCommand.RegisterArtifactTag.class, this::handlRegisterTag) - .onCommand(ACCommand.UpdateArtifactTag.class, this::handleUpdateTag) - .onCommand(ACCommand.RegisterPromotion.class, this::handlePromotionSetting) - ; - return builder.build(); - } - - private ReplyEffect handleRegisterVersion( - final State.ACState state, final ACCommand.RegisterVersion cmd - ) { - if (state.collection().containsKey(cmd.coordinates().version)) { - return this.Effect().reply( - cmd.replyTo(), - new VersionRegistration.Response.ArtifactAlreadyRegistered(cmd.coordinates()) - ); - } - return this.Effect() - .persist(state.addVersion(cmd.coordinates())) - .thenReply(cmd.replyTo(), (s) -> new VersionRegistration.Response.RegisteredArtifact(cmd.coordinates())); - } - - private ReplyEffect handleRegisterArtifact( - final State.Empty state, - final ACCommand.RegisterArtifact cmd - ) { - return this.Effect() - .persist(new ACEvent.ArtifactCoordinatesUpdated(cmd.coordinates())) - .thenReply(cmd.replyTo(), (s) -> NotUsed.notUsed()); - } - - private ReplyEffect handlRegisterTag( - final State.ACState state, - final ACCommand.RegisterArtifactTag cmd - ) { - if (state.tags().containsKey(cmd.entry().name().toLowerCase(Locale.ROOT))) { - return this.Effect().reply( - cmd.replyTo(), new TagRegistration.Response.TagAlreadyRegistered(cmd.entry().name())); - } - return this.Effect() - .persist(new ACEvent.ArtifactTagRegistered(state.coordinates(), cmd.entry())) - .thenReply(cmd.replyTo(), (s) -> new TagRegistration.Response.TagSuccessfullyRegistered()); - } - - private ReplyEffect handlePromotionSetting( - final State.ACState state, - final ACCommand.RegisterPromotion cmd - ) { - return this.Effect() - .persist(new ACEvent.PromotionSettingModified(state.coordinates(), cmd.regex(), cmd.enableManualMarking())) - .thenReply(cmd.replyTo(), (s) -> new TagVersion.Response.TagSuccessfullyRegistered()); - } - - private ReplyEffect handleUpdateTag( - final State.ACState state, - final ACCommand.UpdateArtifactTag cmd - ) { - return this.Effect() - .persist(new ACEvent.ArtifactTagRegistered(state.coordinates(), cmd.entry())) - .thenReply(cmd.replyTo(), (s) -> new TagRegistration.Response.TagSuccessfullyRegistered()); - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactEvent.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactEvent.java deleted file mode 100644 index 0dc24738..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/domain/VersionedArtifactEvent.java +++ /dev/null @@ -1,87 +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.versions.server.domain; - -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.util.Objects; -import java.util.StringJoiner; - -public interface VersionedArtifactEvent extends AggregateEvent, Jsonable { - - AggregateEventShards TAG = AggregateEventTag.sharded(VersionedArtifactEvent.class, 10); - - @Override - default AggregateEventTagger aggregateTag() { - return TAG; - } - - String asMavenCoordinates(); - - class VersionRegistered implements VersionedArtifactEvent { - - public final MavenCoordinates coordinates; - - public VersionRegistered(final MavenCoordinates coordinates) { - this.coordinates = coordinates; - } - - - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - VersionRegistered that = (VersionRegistered) o; - return Objects.equals(coordinates, that.coordinates); - } - - @Override - public int hashCode() { - return Objects.hash(coordinates); - } - - @Override - public String toString() { - return new StringJoiner(", ", VersionRegistered.class.getSimpleName() + "[", "]") - .add("coordinates=" + coordinates) - .toString(); - } - - @Override - public String asMavenCoordinates() { - return this.coordinates.asStandardCoordinates(); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/AssetReadsidePersistence.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/AssetReadsidePersistence.java deleted file mode 100644 index 72936abd..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/AssetReadsidePersistence.java +++ /dev/null @@ -1,120 +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.versions.server.readside; - -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.ReadSide; -import com.lightbend.lagom.javadsl.persistence.ReadSideProcessor; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaReadSide; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaSession; -import org.pcollections.PSequence; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.ArtifactEvent; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.persistence.EntityManager; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.atomic.AtomicInteger; - -@Singleton -public class AssetReadsidePersistence { - - private final JpaSession session; - - @Inject - public AssetReadsidePersistence( - final ReadSide readSide, - final JpaSession session - ) { - this.session = session; - readSide.register(AssetReadsidePersistence.AssetWriter.class); - } - - static final class AssetWriter extends ReadSideProcessor { - - private final JpaReadSide readSide; - - private static final AtomicInteger counter = new AtomicInteger(); - - @Inject - AssetWriter(final JpaReadSide readSide) { - this.readSide = readSide; - } - - @Override - public ReadSideHandler buildHandler() { - return this.readSide.builder("asset_read_side_processor_" + counter.incrementAndGet()) - .setGlobalPrepare((em) -> {}) - .setEventHandler(ArtifactEvent.AssetsUpdated.class, (em, event) -> { - final var coordinates = event.coordinates(); - final var version = em.createNamedQuery( - "ArtifactVersion.findByCoordinates", - JpaArtifactVersion.class - ) - .setParameter("groupId", coordinates.groupId) - .setParameter("artifactId", coordinates.artifactId) - .setParameter("version", coordinates.version) - .setMaxResults(1) - .getSingleResult(); - event.artifacts() - .forEach(asset -> { - final var versionedAsset = findOrCreateVersionedAsset(em, version, asset); - versionedAsset.setDownloadUrl(asset.downloadUrl().toString()); - versionedAsset.setMd5(asset.md5().getBytes(StandardCharsets.UTF_8)); - versionedAsset.setSha1(asset.sha1().getBytes(StandardCharsets.UTF_8)); - versionedAsset.setExtension(asset.extension()); - }); - }) - .build(); - } - - private static JpaVersionedArtifactAsset findOrCreateVersionedAsset( - EntityManager em, JpaArtifactVersion version, Artifact asset - ) { - return em.createNamedQuery( - "VersionedAsset.findByVersion", - JpaVersionedArtifactAsset.class - ) - .setParameter("id", version.getId()) - .setParameter("classifier", asset.classifier().orElse("")) - .setParameter("extension", asset.extension()) - .setMaxResults(1) - .getResultStream() - .findFirst() - .orElseGet(() -> { - final var jpaAsset = new JpaVersionedArtifactAsset(); - jpaAsset.setClassifier(asset.classifier().orElse("")); - version.addAsset(jpaAsset); - return jpaAsset; - }); - } - - @Override - public PSequence> aggregateTags() { - return ArtifactEvent.INSTANCE.allTags(); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifact.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifact.java deleted file mode 100644 index 54d85836..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifact.java +++ /dev/null @@ -1,186 +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.versions.server.readside; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -@Entity(name = "Artifact") -@Table(name = "artifacts", - schema = "version", - indexes = { - @Index(name = "grouped_artifact", - columnList = "group_id, artifact_id", - unique = true) - }) -@NamedQueries({ - @NamedQuery( - name = "Artifact.selectByGroupAndArtifact", - query = """ - select a from Artifact a where a.groupId = :groupId and a.artifactId = :artifactId - """ - ), - @NamedQuery( - name = "Artifact.selectWithTags", - query = """ - select a from Artifact a where a.groupId = :groupId and a.artifactId = :artifactId - """ - ) -}) -public class JpaArtifact implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private int id; - - @Column(name = "group_id", - nullable = false) - private String groupId; - - @Column(name = "artifact_id", - nullable = false) - private String artifactId; - - @OneToMany( - targetEntity = JpaArtifactTag.class, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "artifact") - private Set tags = new HashSet<>(); - - @OneToMany( - targetEntity = JpaArtifactVersion.class, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "artifact") - private Set versions = new HashSet<>(); - - @OneToOne( - targetEntity = JpaArtifactRegexRecommendation.class, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "artifact" - ) - private JpaArtifactRegexRecommendation regexRecommendation; - - @Column(name = "git_repository") - private String repo; - - public int getId() { - return id; - } - - public void setId(final int id) { - this.id = id; - } - - public String getGroupId() { - return groupId; - } - - public void setGroupId(final String groupId) { - this.groupId = groupId; - } - - public String getArtifactId() { - return artifactId; - } - - public void setArtifactId(final String artifactId) { - this.artifactId = artifactId; - } - - public Set getTags() { - return tags; - } - - public void setTags(final Set tags) { - this.tags = tags; - } - - public void addVersion(JpaArtifactVersion version) { - this.versions.add(version); - version.setArtifact(this); - } - - public Set getVersions() { - return versions; - } - - public void setVersions(final Set versions) { - this.versions = versions; - } - - public void setRecommendation( - final JpaArtifactRegexRecommendation regexRecommendation - ) { - this.regexRecommendation = regexRecommendation; - regexRecommendation.setArtifact(this); - } - - public String getRepo() { - return repo; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaArtifact that = (JpaArtifact) o; - return id == that.id && Objects.equals(groupId, that.groupId) && Objects.equals( - artifactId, that.artifactId); - } - - @Override - public int hashCode() { - return Objects.hash(id, groupId, artifactId); - } - - public void addTag(JpaArtifactTag newTag) { - this.tags.add(newTag); - newTag.setArtifact(this); - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactRegexRecommendation.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactRegexRecommendation.java deleted file mode 100644 index 28794554..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactRegexRecommendation.java +++ /dev/null @@ -1,85 +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.versions.server.readside; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; -import javax.persistence.Table; - -@Entity(name = "RegexBasedRecommendation") -@Table(name = "artifact_recommendations", - schema = "version") -@NamedQuery(name = "RegexRecommendation.findByArtifact", - query = """ - select r from RegexBasedRecommendation r where r.artifact.id = :artifactId - """) -public class JpaArtifactRegexRecommendation { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private long id; - - @OneToOne - @JoinColumn(name = "artifact_id", - referencedColumnName = "id", - nullable = false) - private JpaArtifact artifact; - - @Column(name = "recommendation_regex", - nullable = false) - private String regex; - - @Column(name = "allow_manual_promotion") - private boolean manual; - - void setArtifact(JpaArtifact artifact) { - this.artifact = artifact; - } - - public String getRegex() { - return regex; - } - - public void setRegex(final String regex) { - this.regex = regex; - } - - public boolean isManual() { - return manual; - } - - public void setManual(final boolean manual) { - this.manual = manual; - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactTag.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactTag.java deleted file mode 100644 index 98fe79fb..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactTag.java +++ /dev/null @@ -1,98 +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.versions.server.readside; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; - -@Entity(name = "ArtifactTag") -@Table(name = "artifact_tags", - schema = "version") -public class JpaArtifactTag { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private int id; - - @ManyToOne(optional = false, fetch = FetchType.LAZY) - @JoinColumn(name = "artifact_id", referencedColumnName = "id", nullable = false) - private JpaArtifact artifact; - - @Column(name = "tag_name", - nullable = false) - private String name; - - @Column(name = "tag_regex") - private String regex; - - @Column(name = "use_capture_group") - private int group; - - public JpaArtifactTag() { - } - - public JpaArtifact getArtifact() { - return artifact; - } - - void setArtifact(final JpaArtifact taggedVersion) { - this.artifact = taggedVersion; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getRegex() { - return regex; - } - - public void setRegex(final String regex) { - this.regex = regex; - } - - public int getGroup() { - return group; - } - - public void setGroup(final int group) { - this.group = group; - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactVersion.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactVersion.java deleted file mode 100644 index 809581cd..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaArtifactVersion.java +++ /dev/null @@ -1,149 +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.versions.server.readside; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ForeignKey; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; -import java.io.Serializable; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -@Entity(name = "ArtifactVersion") -@Table(name = "artifact_versions", - schema = "version", - uniqueConstraints = @UniqueConstraint( - columnNames = {"artifact_id", "version"}, - name = "artifact_version_unique_idx") -) -@NamedQueries({ - @NamedQuery( - name = "ArtifactVersion.findByVersion", - query = - """ - select distinct v from ArtifactVersion v where v.artifact.id = :artifactId and v.version = :version - """ - ), - - @NamedQuery( - name = "ArtifactVersion.findByCoordinates", - query = - """ - select distinct v from ArtifactVersion v - where v.artifact.groupId = :groupId and v.artifact.artifactId = :artifactId and v.version = :version - """ - ) -}) -class JpaArtifactVersion implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "artifact_id", - foreignKey = @ForeignKey(name = "artifact_versions_artifact_id_fkey"), - nullable = false) - private JpaArtifact artifact; - - @Column(name = "version", - nullable = false) - private String version; - - @Column(name = "ordering") - private int ordering; - - @OneToMany( - targetEntity = JpaVersionedArtifactAsset.class, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "versionedArtifact") - private Set assets = new HashSet<>(); - - void setArtifact(final JpaArtifact artifact) { - this.artifact = artifact; - } - - public JpaArtifact getArtifact() { - return artifact; - } - - public String getVersion() { - return version; - } - - public long getId() { - return id; - } - - public void setVersion(final String version) { - this.version = version; - } - - public void addAsset(final JpaVersionedArtifactAsset asset) { - this.assets.add(asset); - asset.setVersion(this); - } - - public void setOrdering(final int ordering) { - this.ordering = ordering; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaArtifactVersion that = (JpaArtifactVersion) o; - return id == that.id && Objects.equals(artifact, that.artifact) && Objects.equals( - version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(id, artifact, version); - } - -} - diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaVersionedArtifactAsset.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaVersionedArtifactAsset.java deleted file mode 100644 index a2b94925..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/JpaVersionedArtifactAsset.java +++ /dev/null @@ -1,144 +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.versions.server.readside; - -import org.hibernate.annotations.Type; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.Lob; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; -import java.util.Arrays; -import java.util.Objects; - -@Entity(name = "VersionedAsset") -@Table(name = "versioned_assets", - schema = "version") -@NamedQueries({ - @NamedQuery( - name = "VersionedAsset.findByVersion", - query = - """ - select a from VersionedAsset a - where a.versionedArtifact.id = :id and a.classifier = :classifier - and a.extension = :extension - """ - ) -}) -public class JpaVersionedArtifactAsset { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private long id; - - @ManyToOne(targetEntity = JpaArtifactVersion.class, - fetch = FetchType.LAZY) - @JoinColumn(name = "version_id", - referencedColumnName = "id", - nullable = false) - private JpaArtifactVersion versionedArtifact; - - @Column(name = "classifier", - nullable = false) - private String classifier; - - @Column(name = "download_url", - nullable = false) - private String downloadUrl; - - @Column(name = "extension", - nullable = false) - private String extension; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "md5") - private byte[] md5; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "sha1") - private byte[] sha1; - - public void setClassifier(final String classifier) { - this.classifier = classifier; - } - - public void setExtension(final String extension) { - this.extension = extension; - } - - public void setDownloadUrl(final String downloadUrl) { - this.downloadUrl = downloadUrl; - } - - public void setMd5(final byte[] md5) { - this.md5 = md5; - } - - public void setSha1(final byte[] sha1) { - this.sha1 = sha1; - } - - void setVersion(JpaArtifactVersion jpaArtifactVersion) { - this.versionedArtifact = jpaArtifactVersion; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedArtifactAsset that = (JpaVersionedArtifactAsset) o; - return id == that.id && Objects.equals( - versionedArtifact, that.versionedArtifact) && Objects.equals( - classifier, that.classifier) && Objects.equals( - downloadUrl, that.downloadUrl) && Objects.equals( - extension, that.extension) && Arrays.equals( - md5, that.md5) && Arrays.equals(sha1, that.sha1); - } - - @Override - public int hashCode() { - int result = Objects.hash(id, versionedArtifact, classifier, downloadUrl, extension); - result = 31 * result + Arrays.hashCode(md5); - result = 31 * result + Arrays.hashCode(sha1); - return result; - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java deleted file mode 100644 index 7b796485..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionReadSidePersistence.java +++ /dev/null @@ -1,201 +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.versions.server.readside; - -import akka.actor.ActorSystem; -import akka.actor.typed.ActorRef; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.Adapter; -import akka.actor.typed.javadsl.Behaviors; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.ReadSide; -import com.lightbend.lagom.javadsl.persistence.ReadSideProcessor; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaReadSide; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaSession; -import org.pcollections.PSequence; -import org.spongepowered.downloads.versions.server.domain.ACEvent; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.persistence.EntityManager; -import java.util.concurrent.atomic.AtomicInteger; - -@Singleton -public class VersionReadSidePersistence { - - private final JpaSession session; - - @Inject - public VersionReadSidePersistence( - final ReadSide readSide, - final JpaSession session - ) { - this.session = session; - readSide.register(VersionWriter.class); - } - - static final class VersionWriter extends ReadSideProcessor { - - private final JpaReadSide readSide; - private final ActorRef refresher; - - private static final AtomicInteger counter = new AtomicInteger(); - - @Inject - VersionWriter(final JpaReadSide readSide, final JpaSession session, final ActorSystem system) { - this.readSide = readSide; - final var taggedWorker = VersionedTagWorker.create(session); - final var commandBehavior = Behaviors.supervise(taggedWorker).onFailure(SupervisorStrategy.restart()); - this.refresher = Adapter.spawn( - system.classicSystem(), commandBehavior, "version-tag-db-worker-" + counter.incrementAndGet()); - } - - @Override - public ReadSideHandler buildHandler() { - return this.readSide.builder("version-query-builder") - .setGlobalPrepare(this::createSchema) - .setEventHandler(ACEvent.ArtifactCoordinatesUpdated.class, (em, artifactCreated) -> { - System.out.printf("Herpaderp coordinates updated %s\n", artifactCreated.coordinates().toString()); - final var coordinates = artifactCreated.coordinates(); - final var artifactQuery = em.createNamedQuery( - "Artifact.selectByGroupAndArtifact", - JpaArtifact.class - ); - final var singleResult = artifactQuery.setParameter("groupId", coordinates.groupId()) - .setParameter("artifactId", coordinates.artifactId()) - .setMaxResults(1) - .getResultList(); - if (singleResult.isEmpty()) { - final var jpaArtifact = new JpaArtifact(); - jpaArtifact.setGroupId(coordinates.groupId()); - jpaArtifact.setArtifactId(coordinates.artifactId()); - em.persist(jpaArtifact); - } - }) - .setEventHandler(ACEvent.ArtifactVersionRegistered.class, (em, versionRegistered) -> { - final var coordinates = versionRegistered.version(); - final var query = em.createNamedQuery( - "Artifact.selectByGroupAndArtifact", - JpaArtifact.class - ); - query.setParameter("groupId", coordinates.groupId); - query.setParameter("artifactId", coordinates.artifactId); - final var artifact = query.getSingleResult(); - final var version = coordinates.version; - em.createNamedQuery( - "ArtifactVersion.findByVersion", - JpaArtifactVersion.class - ) - .setParameter("artifactId", artifact.getId()) - .setParameter("version", version) - .setMaxResults(1) - .getResultList() - .stream().findFirst() - .orElseGet(() -> { - final var jpaArtifactVersion = new JpaArtifactVersion(); - jpaArtifactVersion.setVersion(version); - jpaArtifactVersion.setOrdering(versionRegistered.sorting()); - artifact.addVersion(jpaArtifactVersion); - refresher.tell(new VersionedTagWorker.RefreshVersionTags()); - refresher.tell(new VersionedTagWorker.RefreshVersionRecommendation(coordinates.asArtifactCoordinates())); - return jpaArtifactVersion; - }); - }) - .setEventHandler(ACEvent.ArtifactTagRegistered.class, (em, tagRegistered) -> { - final var coordinates = tagRegistered.coordinates(); - final var artifactQuery = em.createNamedQuery( - "Artifact.selectWithTags", - JpaArtifact.class - ); - artifactQuery.setParameter("groupId", coordinates.groupId()); - artifactQuery.setParameter("artifactId", coordinates.artifactId()); - final var tag = tagRegistered.entry(); - final var artifact = artifactQuery.getSingleResult(); - final var jpaTag = artifact.getTags().stream() - .filter(jpatag -> jpatag.getName().equals(tag.name())) - .findFirst() - .orElseGet(() -> { - final var jpaArtifactTag = new JpaArtifactTag(); - artifact.addTag(jpaArtifactTag); - return jpaArtifactTag; - }); - jpaTag.setRegex(tag.regex()); - jpaTag.setName(tag.name()); - jpaTag.setGroup(tag.matchingGroup()); - refresher.tell(new VersionedTagWorker.RefreshVersionTags()); - }) - .setEventHandler(ACEvent.PromotionSettingModified.class, (em, promotion) -> { - final var coordinates = promotion.coordinates(); - final var artifactQuery = em.createNamedQuery( - "Artifact.selectWithTags", - JpaArtifact.class - ); - - artifactQuery.setParameter("groupId", coordinates.groupId()); - artifactQuery.setParameter("artifactId", coordinates.artifactId()); - final var artifact = artifactQuery.getSingleResult(); - final var recommendation = em.createNamedQuery( - "RegexRecommendation.findByArtifact", JpaArtifactRegexRecommendation.class) - .setParameter("artifactId", artifact.getId()) - .getResultList() - .stream() - .findFirst() - .orElseGet(() -> { - final var regexRecommendation = new JpaArtifactRegexRecommendation(); - artifact.setRecommendation(regexRecommendation); - return regexRecommendation; - }); - recommendation.setRegex(promotion.regex()); - recommendation.setManual(promotion.enableManualPromotion()); - - refresher.tell(new VersionedTagWorker.RefreshVersionRecommendation(promotion.coordinates())); - }) - .setEventHandler(ACEvent.ArtifactVersionsResorted.class, (em, event) -> { - event.versionordering().forEach((v, ordering) -> { - final var jpaVersion = em.createNamedQuery( - "ArtifactVersion.findByCoordinates", - JpaArtifactVersion.class - ) - .setParameter("groupId", event.coordinates().groupId()) - .setParameter("artifactId", event.coordinates().artifactId()) - .setParameter("version", v) - .setMaxResults(1) - .getSingleResult(); - jpaVersion.setOrdering(ordering); - }); - }) - .build(); - } - - private void createSchema(EntityManager em) { - } - - @Override - public PSequence> aggregateTags() { - return ACEvent.INSTANCE.allTags(); - } - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java deleted file mode 100644 index 204e0cdf..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/server/readside/VersionedTagWorker.java +++ /dev/null @@ -1,146 +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.versions.server.readside; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaSession; -import io.vavr.collection.HashSet; -import io.vavr.collection.Set; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; - -public final class VersionedTagWorker { - - public interface Command { - } - - static final record RefreshVersionTags() implements Command { - } - - static final record RefreshVersionRecommendation(ArtifactCoordinates coordinates) implements Command { - } - - private static final record TimedOut() implements Command { - } - - private static final record Completed(Data data, boolean updatedVersionedTags, int rowsAffected) - implements Command { - } - - private static final record Failed(Data data) implements Command { - } - - private static final record Data(Optional refreshVersions, - Set refreshRecommendations) { - - Data requestedRefreshVersions() { - return new Data(Optional.of(Instant.now()), this.refreshRecommendations); - } - - Data updateArtifactRecommendation(ArtifactCoordinates coordinates) { - return new Data(this.refreshVersions, this.refreshRecommendations.add(coordinates)); - } - } - - public static Behavior create( - final JpaSession session - ) { - return idle(session); - } - - private static Behavior idle(final JpaSession session) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage( - RefreshVersionTags.class, - cmd -> timed(new Data(Optional.of(Instant.now()), HashSet.empty()), session) - ) - .onMessage( - RefreshVersionRecommendation.class, - cmd -> timed(new Data(Optional.empty(), HashSet.of(cmd.coordinates)), session) - ) - .onMessage(Completed.class, cmd -> { - ctx.getLog().info("Completed refresh of {}, affected {}", cmd.data, cmd.rowsAffected); - return Behaviors.same(); - }) - .onMessage(Failed.class, cmd -> { - ctx.getLog().warn("Recovering from failed update, will re-attempt"); - return timed(cmd.data, session); - }) - .build() - ); - } - - private static Behavior timed( - final Data data, final JpaSession session - ) { - return Behaviors.setup(ctx -> Behaviors.withTimers(timers -> { - timers.startSingleTimer(new TimedOut(), Duration.ofSeconds(10)); - return waiting(data, session); - })); - } - - private static Behavior waiting( - final Data data, final JpaSession session - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Command.class) - .onMessage( - RefreshVersionTags.class, - cmd -> waiting(data.requestedRefreshVersions(), session) - ) - .onMessage( - RefreshVersionRecommendation.class, - cmd -> waiting(data.updateArtifactRecommendation(cmd.coordinates), session) - ) - .onMessage(TimedOut.class, timeout -> { - ctx.pipeToSelf(session.withTransaction(em -> { - final var updatedVersionedTags = data.refreshVersions - .map(time -> em - .createNativeQuery("select version.refreshversionedtags()") - .getSingleResult()) - .isPresent(); - final int rowsAffected = data.refreshRecommendations - .map(coordinates -> em.createNativeQuery( - "select version.refreshVersionRecommendations(:artifactId, :groupId)") - .setParameter("artifactId", coordinates.artifactId()) - .setParameter("groupId", coordinates.groupId()) - .getSingleResult()) - .sum().intValue(); - return new Completed(data, updatedVersionedTags, rowsAffected); - }), (msg, throwable) -> { - if (throwable != null) { - ctx.getLog().error("Failed to handle updating artifacts, aborting", throwable); - return new Failed(data); - } - return msg; - }); - return idle(session); - }) - .build()); - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/EntityStore.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/EntityStore.java deleted file mode 100644 index 779384a6..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/EntityStore.java +++ /dev/null @@ -1,37 +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.versions.worker; - -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.Entity; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactEntity; - -public final class EntityStore { - - public static void setupPersistedEntities(ClusterSharding sharding) { - sharding.init(Entity.of(VersionedArtifactEntity.ENTITY_TYPE_KEY, VersionedArtifactEntity::create)); - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionConfig.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionConfig.java deleted file mode 100644 index ebb956b8..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionConfig.java +++ /dev/null @@ -1,52 +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.versions.worker; - -import akka.actor.Extension; -import com.typesafe.config.Config; - -import java.time.Duration; - -public class VersionConfig implements Extension { - - public final CommitFetch commitFetch; - - public VersionConfig(Config config) { - final var commitConfig = config.getConfig("commit-fetch"); - this.commitFetch = new CommitFetch(commitConfig); - } - - public static final class CommitFetch { - public final int poolSize; - public final int parallelism; - public final Duration timeout; - - CommitFetch(Config config) { - this.poolSize = config.getInt("pool-size"); - this.parallelism = config.getInt("parallelism"); - this.timeout = config.getDuration("timeout"); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionExtension.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionExtension.java deleted file mode 100644 index 20624e55..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionExtension.java +++ /dev/null @@ -1,46 +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.versions.worker; - -import akka.actor.AbstractExtensionId; -import akka.actor.ExtendedActorSystem; -import akka.actor.Extension; -import akka.actor.ExtensionId; -import akka.actor.ExtensionIdProvider; - -public class VersionExtension extends AbstractExtensionId implements ExtensionIdProvider { - - public static final VersionExtension Settings = new VersionExtension(); - - @Override - public VersionConfig createExtension(final ExtendedActorSystem system) { - return new VersionConfig(system.settings().config().getConfig("systemofadownload.versions")); - } - - @Override - public ExtensionId lookup() { - return Settings; - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionsWorkerSupervisor.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionsWorkerSupervisor.java deleted file mode 100644 index 7afae9be..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/VersionsWorkerSupervisor.java +++ /dev/null @@ -1,69 +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.versions.worker; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.Cluster; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.versions.api.VersionsService; - -/** - * The "reactive" side of Versions where it performs all the various tasks - * involved with managing Artifact Versions and their artifact assets with - * relation to those versions. Crucially, this performs the various sync jobs - * required to derive a Version from an artifact, as well as the various - * metadata with that version, such as assets and the commit information for - * that asset. This is mostly a guardian actor, one that wires up children - * actors to perform the actual work against topic subscribers, either from - * {@link ArtifactService#artifactUpdate()} or - * {@link VersionsService#artifactUpdateTopic()}. - *

The important reasoning why this is split out from the Version Service - * implementation is that this particular supervisor may well be able to handle - * updates while the VersionsService implementation is the "organizer" of - * root information. - */ -public final class VersionsWorkerSupervisor { - - public static Behavior bootstrapWorkers() { - return Behaviors.setup(ctx -> { - final ClusterSharding sharding = ClusterSharding.get(ctx.getSystem()); - // Persistent EventBased Actors - EntityStore.setupPersistedEntities(sharding); - - // Workers available to do most jobs - final var member = Cluster.get(ctx.getSystem()).selfMember(); - final var system = ctx.getSystem(); - WorkerSpawner.spawnWorkers(system, member, ctx); - - // Finally, self, the supervisor - return Behaviors.receive(Void.class) - .build(); - }); - - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerModule.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerModule.java deleted file mode 100644 index 68c25c64..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerModule.java +++ /dev/null @@ -1,82 +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.versions.worker; - -import akka.actor.ActorSystem; -import akka.actor.typed.ActorRef; -import akka.actor.typed.javadsl.Adapter; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import com.google.inject.AbstractModule; -import com.google.inject.Provider; -import com.google.inject.TypeLiteral; -import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; -import org.spongepowered.downloads.artifact.api.ArtifactService; -import org.spongepowered.downloads.auth.api.utils.AuthUtils; -import org.spongepowered.downloads.versions.api.VersionsService; -import play.Environment; -import play.libs.akka.AkkaGuiceSupport; - -import javax.inject.Inject; - -public class WorkerModule extends AbstractModule implements ServiceGuiceSupport, AkkaGuiceSupport { - - private final AuthUtils auth; - - @SuppressWarnings("unused") // These parameters must match for Play's Guice handling to work. - @Inject - public WorkerModule(final Environment environment, final com.typesafe.config.Config config) { - this.auth = AuthUtils.configure(config); - } - - @Override - protected void configure() { - this.bind(new TypeLiteral>() { - }) - .toProvider(WorkerProvider.class) - .asEagerSingleton(); - - } - - public static record WorkerProvider( - ArtifactService artifacts, - VersionsService versions, - ClusterSharding sharding, - ActorSystem system - ) implements Provider> { - - @Inject - public WorkerProvider { - } - - @Override - public ActorRef get() { - return Adapter.spawn( - this.system, - VersionsWorkerSupervisor.bootstrapWorkers(), - "VersionsWorkerSupervisor" - ); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerSpawner.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerSpawner.java deleted file mode 100644 index a5c4f73d..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/WorkerSpawner.java +++ /dev/null @@ -1,75 +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.versions.worker; - -import akka.actor.typed.ActorSystem; -import akka.actor.typed.DispatcherSelector; -import akka.actor.typed.SupervisorStrategy; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -import akka.actor.typed.receptionist.Receptionist; -import akka.cluster.Member; -import org.spongepowered.downloads.versions.worker.actor.artifacts.CommitExtractor; -import org.spongepowered.downloads.versions.worker.actor.artifacts.FileCollectionOperator; -import org.spongepowered.downloads.versions.worker.actor.delegates.RawCommitReceiver; - -import java.util.UUID; - -public final class WorkerSpawner { - - public static void spawnWorkers(ActorSystem system, Member member, ActorContext ctx) { - // Set up the usual actors - final var versionConfig = VersionExtension.Settings.get(system); - final var poolSizePerInstance = versionConfig.commitFetch.poolSize; - - if (member.hasRole("file-extractor")) { - final var commitFetcherUID = UUID.randomUUID(); - final var behavior = CommitExtractor.extractCommitFromAssets(); - final var assetRefresher = Behaviors.supervise(behavior) - .onFailure(SupervisorStrategy.resume()); - final var pool = Routers.pool(poolSizePerInstance, assetRefresher); - - final var commitExtractorRef = ctx.spawn( - pool, - "file-commit-worker-" + commitFetcherUID, - DispatcherSelector.defaultDispatcher() - ); - // Announce it to the cluster - ctx.getSystem().receptionist().tell(Receptionist.register(CommitExtractor.SERVICE_KEY, commitExtractorRef)); - - final var receiver = Behaviors.supervise(RawCommitReceiver.receive()) - .onFailure(SupervisorStrategy.resume()); - final var receiverRef = ctx.spawn(receiver, "file-scan-result-receiver-" + commitFetcherUID); - final var jarScanner = FileCollectionOperator.scanJarFilesForCommit(commitExtractorRef, receiverRef); - final var supervisedScanner = Behaviors.supervise(jarScanner).onFailure(SupervisorStrategy.resume()); - final var scannerPool = Routers.pool(poolSizePerInstance, supervisedScanner); - final var scannerRef = ctx.spawn(scannerPool, "file-collection-worker-" + commitFetcherUID); - ctx.getSystem().receptionist().tell(Receptionist.register(FileCollectionOperator.KEY, scannerRef)); - } - } - - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/CommitExtractor.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/CommitExtractor.java deleted file mode 100644 index b0a87bee..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/CommitExtractor.java +++ /dev/null @@ -1,238 +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.versions.worker.actor.artifacts; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.receptionist.ServiceKey; -import akka.japi.function.Function2; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.collection.List; -import io.vavr.control.Try; -import org.eclipse.jgit.lib.ObjectId; - -import java.net.URL; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.EnumSet; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.jar.JarInputStream; -import java.util.jar.Manifest; - -/** - * Actor that extracts the commit from a jar file. - */ -public final class CommitExtractor { - - public static final ServiceKey SERVICE_KEY = ServiceKey.create( - ChildCommand.class, "commit-extractor"); - private static final FileAttribute> OWNER_READ_WRITE_ATTRIBUTE = PosixFilePermissions.asFileAttribute( - EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); - - public sealed interface ChildCommand { - } - - public final record AttemptFileCommit( - PotentiallyUsableAsset asset, - ActorRef ref - ) implements ChildCommand { - } - - private final record NoCommitsFound(PotentiallyUsableAsset asset, ActorRef ref) implements ChildCommand { - } - - private final record FailedFile( - Throwable throwable, - PotentiallyUsableAsset asset, - ActorRef replyTo - ) implements ChildCommand { - } - - private final record CommitRetrievedFromFile( - String sha, - PotentiallyUsableAsset asset, - ActorRef replyTo - ) implements ChildCommand { - } - - public sealed interface AssetCommitResponse { - } - - public final record DiscoveredCommitFromFile(String sha, PotentiallyUsableAsset asset) - implements AssetCommitResponse { - } - - - public final record NoCommitsFoundForFile(PotentiallyUsableAsset asset) implements AssetCommitResponse { - } - - public final record FailedToRetrieveCommit(PotentiallyUsableAsset asset) - implements AssetCommitResponse { - } - - public static Behavior extractCommitFromAssets() { - return Behaviors.setup(ctx -> Behaviors.receive(ChildCommand.class) - .onMessage(AttemptFileCommit.class, cmd -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Attempting file commit extraction from {}", cmd.asset); - } - ctx.pipeToSelf(fetchCommitIdFromFile(cmd), handleFileExtractionResult(ctx, cmd)); - return working(cmd.asset, List.empty()); - }) - .build()); - } - - private static Function2, Throwable, ChildCommand> handleFileExtractionResult( - ActorContext ctx, AttemptFileCommit cmd - ) { - return (sha, throwable) -> { - if (throwable != null) { - ctx.getLog().info("Marking file {} as failed", cmd.asset); - return new FailedFile(throwable, cmd.asset, cmd.ref); - } - if (sha.isEmpty()) { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().info("File {} doesn't have a commit", cmd.asset); - } - return new NoCommitsFound(cmd.asset, cmd.ref); - } - final var commit = sha.get(); - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug( - "[{}] Commit extracted from jar {}", cmd.asset.mavenCoordinates().asStandardCoordinates(), - commit.name() - ); - } - return new CommitRetrievedFromFile(commit.name(), cmd.asset, cmd.ref); - }; - } - - - private static Behavior working( - final PotentiallyUsableAsset working, - final List queue - ) { - return Behaviors.setup(ctx -> Behaviors.receive(ChildCommand.class) - .onMessage(AttemptFileCommit.class, cmd -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Received additional work while processing {}", working.mavenCoordinates()); - } - return working(working, queue.append(cmd)); - }) - .onMessage(FailedFile.class, cmd -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Failed commit extraction for " + cmd.asset.mavenCoordinates(), cmd.throwable); - } - cmd.replyTo.tell(new FailedToRetrieveCommit(cmd.asset)); - return swapToNext(ctx, queue); - }) - .onMessage(NoCommitsFound.class, cmd -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("No commits found for " + working.mavenCoordinates()); - } - cmd.ref.tell(new NoCommitsFoundForFile(cmd.asset)); - return swapToNext(ctx, queue); - }) - .onMessage(CommitRetrievedFromFile.class, cmd -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace( - "Commit extracted from {} for {}", cmd.asset.coordinates(), working.mavenCoordinates()); - } - cmd.replyTo.tell(new DiscoveredCommitFromFile(cmd.sha, cmd.asset)); - return swapToNext(ctx, queue); - }) - .build()); - } - - private static Behavior swapToNext( - final ActorContext ctx, - final List queue - ) { - if (queue.isEmpty()) { - return extractCommitFromAssets(); - } - final var next = queue.head(); - ctx.pipeToSelf(fetchCommitIdFromFile(next), handleFileExtractionResult(ctx, next)); - return working(next.asset, queue.tail()); - } - - private static CompletableFuture> fetchCommitIdFromFile( - AttemptFileCommit cmd - ) { - return Try.of(cmd.asset.downloadURL()::toURL) - .mapTry(req -> { - final var tempFile = Files.createTempFile( - String.format( - "commit-check-%s", - cmd.asset.coordinates() - ), - ".jar", - OWNER_READ_WRITE_ATTRIBUTE - ); - return Tuple.of(req, tempFile); - }) - .flatMap(CommitExtractor::getCommitFromFile) - .mapTry(CommitExtractor::extractCommitFromJarManifest) - .toCompletableFuture(); - } - - private static Try getCommitFromFile(Tuple2 tuple) { - return Try.withResources( - () -> Channels.newChannel(tuple._1.openStream()), - () -> FileChannel.open(tuple._2, StandardOpenOption.WRITE) - ) - .of((remoteFile, transfer) -> { - transfer.transferFrom(remoteFile, 0, Long.MAX_VALUE); - return tuple._2; - }); - } - - private static Optional extractCommitFromJarManifest(Path path) { - return Try.withResources( - () -> new JarInputStream(Files.newInputStream(path, StandardOpenOption.DELETE_ON_CLOSE))) - .of(JarInputStream::getManifest) - .filter(Objects::nonNull) - .mapTry(Manifest::getMainAttributes) - .mapTry(attributes -> attributes.getValue("Git-Commit")) - .map(Optional::ofNullable) - .map(opt -> opt.flatMap(sha -> Try.of(() -> ObjectId.fromString(sha)) - .map(Optional::of) - .getOrElse(Optional::empty) - )) - .getOrElse(Optional::empty); - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/FileCollectionOperator.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/FileCollectionOperator.java deleted file mode 100644 index c4755bbc..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/FileCollectionOperator.java +++ /dev/null @@ -1,83 +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.versions.worker.actor.artifacts; - -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.receptionist.ServiceKey; -import akka.stream.javadsl.Sink; -import akka.stream.javadsl.Source; -import akka.stream.typed.javadsl.ActorFlow; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.time.Duration; - -public final class FileCollectionOperator { - - public static final ServiceKey KEY = ServiceKey.create(Request.class, "file-collection-operator"); - - @JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) - @JsonSubTypes({ - @JsonSubTypes.Type(TryFindingCommitForFiles.class) - }) - @JsonDeserialize - public sealed interface Request extends Jsonable {} - - @JsonDeserialize - public static final record TryFindingCommitForFiles( - List files, - MavenCoordinates coordinates - ) implements Request {} - - public static Behavior scanJarFilesForCommit( - final ActorRef commitExtractor, - final ActorRef receiverRef - ) { - return Behaviors.setup(ctx -> Behaviors.receive(Request.class) - .onMessage(TryFindingCommitForFiles.class, msg -> { - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Received request for {}", msg); - } - final List files = msg.files(); - final var from = Source.from(files); - final var extraction = ActorFlow.ask( - 4, - commitExtractor, - Duration.ofMinutes(20), - CommitExtractor.AttemptFileCommit::new - ); - final var receiverSink = Sink.foreach(receiverRef::tell); - from.via(extraction).to(receiverSink).run(ctx.getSystem()); - return Behaviors.same(); - }) - .build()); - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/PotentiallyUsableAsset.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/PotentiallyUsableAsset.java deleted file mode 100644 index 3f186135..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/artifacts/PotentiallyUsableAsset.java +++ /dev/null @@ -1,36 +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.versions.worker.actor.artifacts; - -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.net.URI; - -public record PotentiallyUsableAsset( - MavenCoordinates mavenCoordinates, - String coordinates, - URI downloadURL -) { -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/delegates/RawCommitReceiver.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/delegates/RawCommitReceiver.java deleted file mode 100644 index b1a29844..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/actor/delegates/RawCommitReceiver.java +++ /dev/null @@ -1,70 +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.versions.worker.actor.delegates; - -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.Behaviors; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import org.spongepowered.downloads.versions.worker.actor.artifacts.CommitExtractor; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactCommand; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactEntity; - -public final class RawCommitReceiver { - - public static Behavior receive() { - return Behaviors.setup(ctx -> { - final var sharding = ClusterSharding.get(ctx.getSystem()); - return Behaviors.receive(CommitExtractor.AssetCommitResponse.class) - .onMessage(CommitExtractor.FailedToRetrieveCommit.class, msg -> { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug( - "[{}] Failed to retrieve commit", msg.asset().mavenCoordinates().asStandardCoordinates()); - } - sharding.entityRefFor(VersionedArtifactEntity.ENTITY_TYPE_KEY, msg.asset().mavenCoordinates().asStandardCoordinates()) - .tell(new VersionedArtifactCommand.MarkFilesAsErrored()); - return Behaviors.same(); - }) - .onMessage(CommitExtractor.DiscoveredCommitFromFile.class, msg -> { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug( - "[{}] Retrieved commit", msg.asset().mavenCoordinates().asStandardCoordinates()); - } - sharding.entityRefFor(VersionedArtifactEntity.ENTITY_TYPE_KEY, msg.asset().mavenCoordinates().asStandardCoordinates()) - .tell(new VersionedArtifactCommand.RegisterRawCommit(msg.sha())); - return Behaviors.same(); - }) - .onMessage(CommitExtractor.NoCommitsFoundForFile.class, msg -> { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug( - "[{}] No commit found", msg.asset().mavenCoordinates().asStandardCoordinates()); - } - sharding.entityRefFor(VersionedArtifactEntity.ENTITY_TYPE_KEY, msg.asset().mavenCoordinates().asStandardCoordinates()) - .tell(new VersionedArtifactCommand.MarkFilesAsErrored()); - return Behaviors.same(); - }) - .build(); - }); - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactEvent.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactEvent.java deleted file mode 100644 index 191c6176..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactEvent.java +++ /dev/null @@ -1,82 +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.versions.worker.domain.versionedartifact; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.lightbend.lagom.javadsl.persistence.AggregateEvent; -import com.lightbend.lagom.javadsl.persistence.AggregateEventShards; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTagger; -import com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION) -public sealed interface ArtifactEvent extends AggregateEvent, Jsonable { - - AggregateEventShards INSTANCE = AggregateEventTag.sharded(ArtifactEvent.class, 3); - - @Override - default AggregateEventTagger aggregateTag() { - return INSTANCE; - } - - final record Registered(MavenCoordinates coordinates) implements ArtifactEvent { - } - - final record AssetsUpdated(MavenCoordinates coordinates, List artifacts) implements ArtifactEvent { - } - - final record FilesErrored() implements ArtifactEvent { - } - - final record CommitAssociated(MavenCoordinates coordinates, List repos, String commitSha) - implements ArtifactEvent { - @JsonCreator - public CommitAssociated { - } - } - - final record CommitResolved( - MavenCoordinates coordinates, - URI repo, - VersionedCommit versionedCommit - ) implements ArtifactEvent { - } - - public record CommitUnresolved( - MavenCoordinates coordinates, - String commitId - ) - implements ArtifactEvent { - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactState.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactState.java deleted file mode 100644 index 8b76931c..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/ArtifactState.java +++ /dev/null @@ -1,249 +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.versions.worker.domain.versionedartifact; - -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 com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.HashSet; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.function.Predicate; - -@JsonDeserialize -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = ArtifactState.Unregistered.class, name = "unregistered"), - @JsonSubTypes.Type(value = ArtifactState.Registered.class, name = "registered") -}) -public sealed interface ArtifactState extends Jsonable { - - io.vavr.collection.List artifacts(); - - default boolean needsArtifactScan() { - return false; - } - - default boolean needsCommitResolution() { - return false; - } - - default Optional commitSha() { - return Optional.empty(); - } - - default io.vavr.collection.List repositories() { - return io.vavr.collection.List.empty(); - } - - final record Unregistered() implements ArtifactState { - private static final Unregistered INSTANCE = new Unregistered(); - - public ArtifactEvent register(VersionedArtifactCommand.Register cmd) { - return new ArtifactEvent.Registered(cmd.coordinates()); - } - - @Override - public io.vavr.collection.List artifacts() { - return io.vavr.collection.List.empty(); - } - } - - @JsonDeserialize - final record FileStatus( - Optional commitSha, - Optional commit, - boolean scanned, - io.vavr.collection.List artifacts, - boolean resolutionError) { - @JsonCreator - public FileStatus { - } - - static final FileStatus EMPTY = new FileStatus( - Optional.empty(), - Optional.empty(), - true, - io.vavr.collection.List.empty(), - false - ); - - public FileStatus withResultionError(boolean b) { - return new FileStatus( - commitSha, - commit, - scanned, - artifacts, - b - ); - } - } - - static Registered register(final ArtifactEvent.Registered event) { - return new Registered(event.coordinates(), HashSet.empty(), FileStatus.EMPTY); - } - - final record Registered( - MavenCoordinates coordinates, - HashSet repo, - FileStatus fileStatus - ) implements ArtifactState { - - public Registered { - } - - @Override - public Optional commitSha() { - return this.fileStatus.commitSha; - } - - @Override - public boolean needsArtifactScan() { - return !this.fileStatus.scanned; - } - - @Override - public boolean needsCommitResolution() { - return this.fileStatus.commit.isEmpty() && this.fileStatus.commitSha.isPresent(); - } - - @Override - public io.vavr.collection.List repositories() { - return this.repo.toList().map(URI::create); - } - - public List addAssets(io.vavr.collection.List artifacts) { - - final var filtered = artifacts - .filter(Predicate.not(a -> this.fileStatus.artifacts.map(Artifact::downloadUrl).contains(a.downloadUrl()))); - if (filtered.isEmpty()) { - return List.of(); - } - - return List.of(new ArtifactEvent.AssetsUpdated(this.coordinates, filtered)); - } - - public ArtifactState withAssets(io.vavr.collection.List artifacts) { - return new Registered( - this.coordinates, - this.repo, - new FileStatus( - this.fileStatus.commitSha, - this.fileStatus.commit, - this.fileStatus.commit.isPresent() || this.fileStatus.commitSha.isPresent(), - artifacts, - false - ) - ); - } - - @Override - public io.vavr.collection.List artifacts() { - return this.fileStatus.artifacts(); - } - - public ArtifactState markFilesErrored() { - return new Registered( - this.coordinates, - this.repo, - new FileStatus( - this.fileStatus().commitSha, - this.fileStatus.commit, - true, - this.fileStatus.artifacts, - false - ) - ); - } - - public List associateCommit(String commitSha) { - if (this.fileStatus.commitSha.isPresent()) { - return List.of(); - } - return List.of(new ArtifactEvent.CommitAssociated(this.coordinates, this.repo.toList(), commitSha)); - } - - public ArtifactState withCommit(String commitSha) { - if (this.fileStatus.commitSha.isPresent()) { - return this; - } - return new Registered( - this.coordinates, - this.repo, - new FileStatus( - Optional.of(commitSha), - this.fileStatus.commit, - true, - this.fileStatus.artifacts, - false - ) - ); - } - - public ArtifactState resolveCommit(ArtifactEvent.CommitResolved event) { - return new Registered( - this.coordinates, - this.repo, - new FileStatus( - this.fileStatus.commitSha, - Optional.of(event.versionedCommit()), - this.fileStatus.scanned, - this.fileStatus.artifacts, - false - ) - ); - } - - public ArtifactState withRepository(String repository) { - return new Registered( - this.coordinates, - this.repo.add(repository), - this.fileStatus - ); - } - - public List failedCommit(String commitId) { - return this.fileStatus.commit.map(v -> Collections.emptyList()) - .orElseGet(() -> List.of(new ArtifactEvent.CommitUnresolved(this.coordinates, commitId))); - } - - public ArtifactState markCommitAsUnresolved(ArtifactEvent.CommitUnresolved a) { - final var status = this.fileStatus.withResultionError(true); - return new Registered( - this.coordinates, - this.repo, - status - ); - } - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactCommand.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactCommand.java deleted file mode 100644 index 25e7b6e9..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactCommand.java +++ /dev/null @@ -1,112 +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.versions.worker.domain.versionedartifact; - -import akka.Done; -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 com.lightbend.lagom.serialization.Jsonable; -import io.vavr.collection.List; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.ArtifactCollection; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; - -import java.net.URI; - -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = VersionedArtifactCommand.Register.class, name = "register"), - @JsonSubTypes.Type(value = VersionedArtifactCommand.AddAssets.class, name = "add-assets"), - @JsonSubTypes.Type(value = VersionedArtifactCommand.MarkFilesAsErrored.class, name = "errored-assets"), - @JsonSubTypes.Type(value = VersionedArtifactCommand.RegisterAssets.class, name = "register-assets"), - @JsonSubTypes.Type(value = VersionedArtifactCommand.RegisterRawCommit.class, name = "register-sha"), - @JsonSubTypes.Type(value = VersionedArtifactCommand.RegisterResolvedCommit.class, name = "register-commit"), -}) -@JsonDeserialize -public sealed interface VersionedArtifactCommand extends Jsonable { - - final record Register( - MavenCoordinates coordinates, - ActorRef replyTo - ) implements VersionedArtifactCommand { - @JsonCreator - public Register { - } - } - - record RegisterAssets( - MavenCoordinates coordinates, - ArtifactCollection collection, - ActorRef replyTo - ) implements VersionedArtifactCommand { - @JsonCreator - public RegisterAssets { - } - } - - final record AddAssets( - MavenCoordinates coordinates, - List artifacts, - ActorRef replyTo - ) implements VersionedArtifactCommand { - @JsonCreator - public AddAssets { - } - } - - final record MarkFilesAsErrored() implements VersionedArtifactCommand { - @JsonCreator - public MarkFilesAsErrored { - } - } - - final record RegisterRawCommit(String commitSha) implements VersionedArtifactCommand {} - - final record RegisterResolvedCommit( - VersionedCommit versionedCommit, - URI repo, - ActorRef replyTo - ) - implements VersionedArtifactCommand { - @JsonCreator - public RegisterResolvedCommit { - } - } - - record RegisterFailedCommit( - String commitId, - URI repo, - ActorRef replyTo - ) - implements VersionedArtifactCommand { - } - - public record RefreshCommitResolution() implements VersionedArtifactCommand { } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactEntity.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactEntity.java deleted file mode 100644 index 7ff0ebf0..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/domain/versionedartifact/VersionedArtifactEntity.java +++ /dev/null @@ -1,306 +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.versions.worker.domain.versionedartifact; - -import akka.Done; -import akka.actor.typed.ActorRef; -import akka.actor.typed.Behavior; -import akka.actor.typed.javadsl.ActorContext; -import akka.actor.typed.javadsl.Behaviors; -import akka.actor.typed.javadsl.Routers; -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 akka.persistence.typed.javadsl.ReplyEffect; -import akka.persistence.typed.javadsl.RetentionCriteria; -import com.lightbend.lagom.javadsl.persistence.AkkaTaggerAdapter; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.versions.api.models.VersionRegistration; -import org.spongepowered.downloads.versions.worker.actor.artifacts.FileCollectionOperator; -import org.spongepowered.downloads.versions.worker.actor.artifacts.PotentiallyUsableAsset; - -import java.util.Arrays; -import java.util.Set; -import java.util.function.Function; - -public class VersionedArtifactEntity - extends EventSourcedBehaviorWithEnforcedReplies { - public static EntityTypeKey ENTITY_TYPE_KEY = EntityTypeKey.create( - VersionedArtifactCommand.class, "VersionedArtifactEntity"); - private final Function> tagger; - private final ActorRef scanFiles; - private final ActorContext ctx; - - public static Behavior create(final EntityContext context) { - return Behaviors.setup(ctx -> { - final var scanFiles = Routers.group(FileCollectionOperator.KEY); - final var scanRef = ctx.spawn(scanFiles, "scan-files-" + context.getEntityId()); - if (ctx.getLog().isTraceEnabled()) { - ctx.getLog().trace("Entity Context {}", context.getEntityId()); - } - return new VersionedArtifactEntity(context, ctx, scanRef); - }); - } - - private VersionedArtifactEntity( - final EntityContext entityContext, - final ActorContext ctx, - final ActorRef scanRef - ) { - super(PersistenceId.of(entityContext.getEntityTypeKey().name(), entityContext.getEntityId())); - this.ctx = ctx; - this.tagger = AkkaTaggerAdapter.fromLagom(entityContext, ArtifactEvent.INSTANCE); - this.scanFiles = scanRef; - } - - @Override - public ArtifactState emptyState() { - return new ArtifactState.Unregistered(); - } - - @Override - public EventHandler eventHandler() { - final var builder = this.newEventHandlerBuilder(); - builder.forStateType(ArtifactState.Unregistered.class) - .onEvent(ArtifactEvent.Registered.class, ArtifactState::register) - ; - builder.forStateType(ArtifactState.Registered.class) - .onEvent(ArtifactEvent.AssetsUpdated.class, (s, e) -> s.withAssets(e.artifacts())) - .onEvent(ArtifactEvent.FilesErrored.class, (s, e) -> s.markFilesErrored()) - .onEvent(ArtifactEvent.CommitAssociated.class, (s, e) -> s.withCommit(e.commitSha())) - .onEvent(ArtifactEvent.CommitResolved.class, ArtifactState.Registered::resolveCommit) - .onEvent(ArtifactEvent.CommitUnresolved.class, ArtifactState.Registered::markCommitAsUnresolved) - ; - return builder.build(); - } - - @Override - public CommandHandlerWithReply commandHandler() { - final var builder = this.newCommandHandlerWithReplyBuilder(); - builder.forStateType(ArtifactState.Unregistered.class) - .onCommand(VersionedArtifactCommand.Register.class, this::onRegister) - .onCommand(VersionedArtifactCommand.AddAssets.class, this::onEmptyAddAssets) - .onCommand(VersionedArtifactCommand.RegisterAssets.class, this::onEmptyRegisterAssets) - ; - builder.forStateType(ArtifactState.Registered.class) - .onCommand(VersionedArtifactCommand.Register.class, cmd -> this.Effect().reply(cmd.replyTo(), Done.done())) - .onCommand(VersionedArtifactCommand.RegisterAssets.class, this::onRegisterAssets) - .onCommand(VersionedArtifactCommand.AddAssets.class, this::onAddAssets) - .onCommand(VersionedArtifactCommand.MarkFilesAsErrored.class, this::onMarkFilesAsErrored) - .onCommand(VersionedArtifactCommand.RegisterRawCommit.class, this::onRegisterRawCommit) - .onCommand(VersionedArtifactCommand.RegisterResolvedCommit.class, this::handleCompletedCommit) - .onCommand(VersionedArtifactCommand.RegisterFailedCommit.class, this::handleFailedCommit) - .onCommand(VersionedArtifactCommand.RefreshCommitResolution.class, this::handleRefreshCommitStatus) - ; - return builder.build(); - } - - private ReplyEffect handleRefreshCommitStatus(final ArtifactState.Registered state, final VersionedArtifactCommand.RefreshCommitResolution cmd) { - if (state.fileStatus().commit().isPresent()) { - return this.Effect().noReply(); - } - if (state.commitSha().isEmpty()) { - return this.Effect().noReply(); - } - return this.Effect() - .persist(new ArtifactEvent.CommitAssociated(state.coordinates(), state.repo().toList(), state.commitSha().get())) - .thenNoReply(); - } - - private ReplyEffect handleFailedCommit( - final ArtifactState.Registered state, - final VersionedArtifactCommand.RegisterFailedCommit cmd - ) { - if (ctx.getLog().isDebugEnabled()) { - ctx.getLog().debug( - "[{}] Commit {} failed to resolve", state.coordinates().asStandardCoordinates(), cmd.commitId()); - } - return this.Effect() - .persist(state.failedCommit(cmd.commitId())) - .thenReply(cmd.replyTo(), ns -> Done.done()); - } - - private ReplyEffect onRegisterAssets( - final ArtifactState.Registered state, - final VersionedArtifactCommand.RegisterAssets cmd - ) { - final var artifacts = cmd.collection().components(); - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("[{}] Current assets: {}", state.coordinates(), state.artifacts()); - this.ctx.getLog().trace("[{}] Adding assets {}", state.coordinates(), artifacts); - } - return this.Effect() - .persist(state.addAssets(artifacts)) - .thenRun(ns -> { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("[{}] Updated assets", state.coordinates()); - } - if (ns.needsArtifactScan()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug( - "[{}] Telling FileCollectionOperator to fetch commit from files", - state.coordinates() - ); - } - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace( - "[{}] Assets available {}", state.coordinates(), ns.artifacts().map(Artifact::toString)); - } - final var usableAssets = ns.artifacts() - .filter(a -> "jar".equalsIgnoreCase(a.extension())) - .filter(a -> a.classifier().isEmpty() || a.classifier().filter(""::equals).isPresent()) - .map(a -> new PotentiallyUsableAsset(state.coordinates(), a.extension(), a.downloadUrl())); - if (!usableAssets.isEmpty()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug("[{}] Assets to scan {}", state.coordinates(), usableAssets); - } - this.scanFiles.tell( - new FileCollectionOperator.TryFindingCommitForFiles(usableAssets, state.coordinates())); - } - } - }) - .thenReply(cmd.replyTo(), ns -> new VersionRegistration.Response.RegisteredArtifact(cmd.coordinates())); - } - - private ReplyEffect onEmptyRegisterAssets( - final VersionedArtifactCommand.RegisterAssets cmd - ) { - this.ctx.getLog().warn("[{}] Registering collection on empty state", cmd.coordinates().asStandardCoordinates()); - return this.Effect() - .persist(Arrays.asList(new ArtifactEvent.Registered(cmd.coordinates()), new ArtifactEvent.AssetsUpdated(cmd.coordinates(), cmd.collection().components()))) - .thenReply(cmd.replyTo(), ns -> new VersionRegistration.Response.RegisteredArtifact(cmd.coordinates())); - } - - private ReplyEffect onEmptyAddAssets( - final VersionedArtifactCommand.AddAssets cmd - ) { - this.ctx.getLog().warn("[{}] Registering assets with empty state", cmd.coordinates().asStandardCoordinates()); - return this.Effect() - .persist(Arrays.asList(new ArtifactEvent.Registered(cmd.coordinates()), new ArtifactEvent.AssetsUpdated(cmd.coordinates(), cmd.artifacts()))) - .thenReply(cmd.replyTo(), ns -> Done.done()); - } - - private ReplyEffect handleCompletedCommit( - final ArtifactState.Registered state, final VersionedArtifactCommand.RegisterResolvedCommit cmd - ) { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("Received completed commit {}", cmd.versionedCommit()); - } - if (state.fileStatus().commit().isPresent()) { - this.ctx.getLog().warn("[{}] Ignoring {} with already registered commit {}", state.coordinates().asStandardCoordinates(), cmd.versionedCommit(), state.fileStatus().commit().get()); - return this.Effect().reply(cmd.replyTo(), Done.done()); - } - return this.Effect() - .persist(new ArtifactEvent.CommitResolved(state.coordinates(), cmd.repo(), cmd.versionedCommit())) - .thenReply(cmd.replyTo(), ns -> Done.done()); - } - - private ReplyEffect onRegisterRawCommit( - final ArtifactState.Registered state, - final VersionedArtifactCommand.RegisterRawCommit cmd - ) { - this.ctx.getLog().debug("Raw commit registered {}", cmd); - return this.Effect() - .persist(state.associateCommit(cmd.commitSha())) - .thenNoReply(); - } - - private ReplyEffect onMarkFilesAsErrored(ArtifactState state, VersionedArtifactCommand.MarkFilesAsErrored cmd) { - if (state.needsArtifactScan()) { - return this.Effect() - .persist(new ArtifactEvent.FilesErrored()) - .thenRun(ns -> this.ctx.getLog().debug("File as failed {}", ns)) - .thenNoReply(); - } - return this.Effect() - .persist(new ArtifactEvent.FilesErrored()) - .thenRun(ns -> this.ctx.getLog().debug("File as failed {}", ns)) - .thenNoReply(); - } - - private ReplyEffect onAddAssets( - final ArtifactState.Registered state, - final VersionedArtifactCommand.AddAssets cmd - ) { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("[{}] Current assets: {}", state.coordinates(), state.artifacts()); - this.ctx.getLog().trace("[{}] Adding assets {}", state.coordinates(), cmd.artifacts()); - } - return this.Effect() - .persist(state.addAssets(cmd.artifacts())) - .thenRun(ns -> { - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace("[{}] Updated assets", state.coordinates()); - } - if (ns.needsArtifactScan()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug( - "[{}] Telling FileCollectionOperator to fetch commit from files", - state.coordinates() - ); - } - if (this.ctx.getLog().isTraceEnabled()) { - this.ctx.getLog().trace( - "[{}] Assets available {}", state.coordinates(), ns.artifacts().map(Artifact::toString)); - } - final var usableAssets = ns.artifacts() - .filter(a -> "jar".equalsIgnoreCase(a.extension())) - .filter(a -> a.classifier().isEmpty() || a.classifier().filter(""::equals).isPresent()) - .map(a -> new PotentiallyUsableAsset(state.coordinates(), a.extension(), a.downloadUrl())); - if (!usableAssets.isEmpty()) { - if (this.ctx.getLog().isDebugEnabled()) { - this.ctx.getLog().debug("[{}] Assets to scan {}", state.coordinates(), usableAssets); - } - this.scanFiles.tell( - new FileCollectionOperator.TryFindingCommitForFiles(usableAssets, state.coordinates())); - } - } - }) - .thenReply(cmd.replyTo(), ns -> Done.done()); - } - - private ReplyEffect onRegister( - final ArtifactState.Unregistered state, - final VersionedArtifactCommand.Register cmd - ) { - return this.Effect() - .persist(state.register(cmd)) - .thenReply(cmd.replyTo(), ns -> Done.done()); - } - - - @Override - public Set tagsFor(final ArtifactEvent assetEvent) { - return this.tagger.apply(assetEvent); - } - - @Override - public RetentionCriteria retentionCriteria() { - return RetentionCriteria.snapshotEvery(5, 2); - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/intro.md b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/intro.md deleted file mode 100644 index 11aef813..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/intro.md +++ /dev/null @@ -1,40 +0,0 @@ -# Versions Worker - -Versions Worker is a collection of workers that perform versioned artifact introspection. -The primary use case is to organize all of the reactive pieces of work that come about from -"a new version is registered". The workers are designed to be pluggable, and the -[`VersionsWorkerSupervisor`](VersionsWorkerSupervisor.java) is the main entry point for this -side of the system. - -## Overview - -While a normal Lagom application is a single service with a single Guice module, the -Versions Worker has a separate paired module to initialize the guardian of the child worker -actors. There is an additional side effect that is gained out of this: the supervisor is able -to reference the VersionsService indirectly as a consumer, and therefore subscribe to the -[`VersionsService.artifactUpdateTopic()`](./../../../../../../../../../versions-api/src/main/java/org/spongepowered/downloads/versions/api/VersionsService.java) topic. -An advantage as well is that while each binary of the workers are capable of delegating their -work to avoid blocking eachother, the VersionsService remains active for the primitive information -as a query reference. - -## Interfaces between systems - -### Kafka - -Through heavy use of Akka Clustering and Akka remoting, we can spin off specific workloads to -specific instances, such as git commit resolution, or artifact binary downloading and processing, -or changelog parsing. To safely coordinate the flow of work and prevent soft failures, we rely heavily -on the TopicSubscriber pattern to have the assurance the work will be done, even if the actors/binaries -restart for any reason. Likewise, we can coordinate the flow of work to single instances for longer -running jobs, while allowing for rapid fire handling of "basic" data being transformed and -messages sent off. - -### ReadSideProcessors - -We take advantage of EventSourceBased actors to describe the state of the various entities (such as -a versioned artifact with assets not having a git commit, to becoming a git commit hash extracted -from the binary, to extracting the commit details from the git repository, to resolving the changes -between two ordinal versions) and take advantage that we can populate our database with the important -details from the various events, therefor enabling the Query service to simply query for the state of -whatever models it is interested in. - diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/CommitProcessor.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/CommitProcessor.java deleted file mode 100644 index db808b8d..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/CommitProcessor.java +++ /dev/null @@ -1,214 +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.versions.worker.readside; - -import akka.actor.ActorSystem; -import com.lightbend.lagom.javadsl.persistence.AggregateEventTag; -import com.lightbend.lagom.javadsl.persistence.ReadSide; -import com.lightbend.lagom.javadsl.persistence.ReadSideProcessor; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaReadSide; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaSession; -import io.vavr.collection.List; -import io.vavr.control.Try; -import org.pcollections.PSequence; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.api.models.VersionedChangelog; -import org.spongepowered.downloads.versions.api.models.VersionedCommit; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.ArtifactEvent; -import org.spongepowered.downloads.versions.worker.readside.model.JpaVersionChangelog; -import org.spongepowered.downloads.versions.worker.readside.model.JpaVersionedArtifact; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.persistence.EntityManager; -import java.net.URI; -import java.net.URL; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.util.Optional; - -@Singleton -public final record CommitProcessor( - ReadSide readSide, - JpaSession session -) { - - @Inject - public CommitProcessor { - readSide.register(CommitWriter.class); - } - - static final class CommitWriter extends ReadSideProcessor { - - private static final Logger LOGGER = LoggerFactory.getLogger("CommitWriter"); - public static final ZonedDateTime EPOCH = ZonedDateTime.of(LocalDateTime.MIN, ZoneOffset.UTC); - - private final JpaReadSide readSide; - - @Inject - CommitWriter(final JpaReadSide readSide, final JpaSession session, final ActorSystem system) { - this.readSide = readSide; - } - - @Override - public ReadSideHandler buildHandler() { - return this.readSide.builder("version-commit-writer") - .setGlobalPrepare((em) -> { - }) - .setEventHandler(ArtifactEvent.FilesErrored.class, (em, e) -> { - }) - .setEventHandler(ArtifactEvent.Registered.class, (em, e) -> { - }) - .setEventHandler(ArtifactEvent.AssetsUpdated.class, (em, e) -> { - }) - .setEventHandler( - ArtifactEvent.CommitAssociated.class, - (em, e) -> { - final var coordinates = e.coordinates(); - final var results = getVersionedArtifacts(em, coordinates); - if (results.isEmpty()) { - return; - } - final JpaVersionedArtifact jpaVersionedArtifact = results.get(0); - if (jpaVersionedArtifact.getChangelog() == null) { - final var jpaVersionChangelog = new JpaVersionChangelog(); - jpaVersionedArtifact.setChangelog(jpaVersionChangelog); - } - final var jpaChangelog = jpaVersionedArtifact.getChangelog(); - final var author = new VersionedCommit.Author("", ""); - final var committer = new VersionedCommit.Commiter("", ""); - final ZonedDateTime epoch = ZonedDateTime.of( - LocalDate.EPOCH, LocalTime.MAX, ZoneId.systemDefault()); - final var artifactRepo = jpaVersionedArtifact.getArtifact().getRepo(); - final var commitLink = Optional.ofNullable(artifactRepo).map(URI::create); - final VersionedCommit rawCommit = new VersionedCommit( - "", "", e.commitSha(), author, committer, commitLink, epoch); - final var changelog = new VersionedChangelog( - List.of(new VersionedChangelog.IndexedCommit(rawCommit, List.empty())), true); - jpaChangelog.setSha(rawCommit.sha()); - jpaChangelog.setChangelog(changelog); - Try.ofSupplier(commitLink::get) - .mapTry(URI::toURL) - .toJavaOptional() - .ifPresent(jpaChangelog::setRepo); - em.persist(jpaChangelog); - } - ) - .setEventHandler( - ArtifactEvent.CommitResolved.class, - (em, e) -> { - final var coordinates = e.coordinates(); - final var results = getVersionedArtifacts(em, coordinates); - if (results.isEmpty()) { - return; - } - final JpaVersionedArtifact jpaVersionedArtifact = results.get(0); - if (jpaVersionedArtifact.getChangelog() == null) { - final var jpaVersionChangelog = new JpaVersionChangelog(); - jpaVersionedArtifact.setChangelog(jpaVersionChangelog); - jpaVersionChangelog.setSha(e.versionedCommit().sha()); - jpaVersionChangelog.setBranch("foo"); - final var commitUrl = e.repo().toString().replace(".git", "") + "/commit/" + e.versionedCommit().sha(); - Try.of(() -> new URL(commitUrl)) - .toJavaOptional() - .ifPresent(jpaVersionChangelog::setRepo); - } - final var jpaChangelog = jpaVersionedArtifact.getChangelog(); - final var commit = new VersionedChangelog.IndexedCommit(e.versionedCommit(), List.empty()); - final var changelog = new VersionedChangelog(List.of(commit), true); - jpaChangelog.setChangelog(changelog); - em.persist(jpaChangelog); - } - ) - .setEventHandler( - ArtifactEvent.CommitUnresolved.class, - (em, e) -> { - final var coordinates = e.coordinates(); - final var results = getVersionedArtifacts(em, coordinates); - if (results.isEmpty()) { - return; - } - final JpaVersionedArtifact jpaVersionedArtifact = results.get(0); - final var changelog = new VersionedChangelog(List.of( - new VersionedChangelog.IndexedCommit( - convertToInvalidCommit(e), List.empty() - )), false); - if (jpaVersionedArtifact.getChangelog() == null) { - final var jpaVersionChangelog = new JpaVersionChangelog(); - jpaVersionedArtifact.setChangelog(jpaVersionChangelog); - jpaVersionChangelog.setSha(e.commitId()); - jpaVersionChangelog.setBranch("foo"); - jpaVersionChangelog.setChangelog(changelog); - } - final var jpaChangelog = jpaVersionedArtifact.getChangelog(); - jpaChangelog.setChangelog(changelog); - em.persist(jpaChangelog); - } - ) - .build(); - } - - private static VersionedCommit convertToInvalidCommit(ArtifactEvent.CommitUnresolved e) { - return new VersionedCommit( - "Commit not available", - """ - This build has a commit that cannot be resolved, it may be - possible to resolve it in some backup, but generally this - build will not be supported by the community due to the - lack of a commit. - """, - e.commitId(), - new VersionedCommit.Author("", ""), - new VersionedCommit.Commiter("", ""), - Optional.empty(), - EPOCH - ); - } - - private java.util.List getVersionedArtifacts( - EntityManager em, MavenCoordinates coordinates - ) { - return em.createNamedQuery( - "GitVersionedArtifact.findByCoordinates", JpaVersionedArtifact.class) - .setParameter("groupId", coordinates.groupId) - .setParameter("artifactId", coordinates.artifactId) - .setParameter("version", coordinates.version) - .setMaxResults(1) - .getResultList(); - } - - @Override - public PSequence> aggregateTags() { - return ArtifactEvent.INSTANCE.allTags(); - } - } - -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionChangelog.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionChangelog.java deleted file mode 100644 index 9a43464d..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionChangelog.java +++ /dev/null @@ -1,132 +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.versions.worker.readside.model; - -import com.vladmihalcea.hibernate.type.json.JsonBinaryType; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.hibernate.annotations.TypeDefs; -import org.spongepowered.downloads.versions.api.models.VersionedChangelog; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.MapsId; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.net.URL; -import java.util.Objects; - -@Entity(name = "VersionedChangelog") -@Table(name = "version_changelogs", schema = "version") -@TypeDefs({ - @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) -}) -public class JpaVersionChangelog { - - @Id - @Column(name = "version_id") - private long id; - - @OneToOne(mappedBy = "changelog", optional = false) - @MapsId("id") - private JpaVersionedArtifact artifact; - - @Column(name = "commit_sha", nullable = false) - private String sha; - - @Type(type = "jsonb") - @Column(name = "changelog", columnDefinition = "jsonb") - private VersionedChangelog changelog; - - @Column(name = "repo") - private URL repo; - - @Column(name = "branch") - private String branch; - - public long getId() { - return id; - } - - void setId(long id) { - this.id = id; - } - - public void setSha(final String sha) { - this.sha = sha; - } - - public void setRepo(final URL repo) { - this.repo = repo; - } - - public void setBranch(final String branch) { - this.branch = branch; - } - - public JpaVersionedArtifact getArtifact() { - return artifact; - } - - void setArtifact(JpaVersionedArtifact artifact) { - this.artifact = artifact; - } - - public void setChangelog(VersionedChangelog changelog) { - this.changelog = changelog; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionChangelog that = (JpaVersionChangelog) o; - return id == that.id && Objects.equals(artifact, that.artifact) && Objects.equals( - sha, that.sha) && Objects.equals(changelog, that.changelog) && Objects.equals( - repo, that.repo) && Objects.equals(branch, that.branch); - } - - @Override - public int hashCode() { - return Objects.hash(id, artifact, sha, changelog, repo, branch); - } - - @Override - public String toString() { - return "JpaVersionChangelog{" + - "id=" + id + - ", artifact=" + artifact + - ", sha='" + sha + '\'' + - ", changelog=" + changelog + - ", repo=" + repo + - ", branch='" + branch + '\'' + - '}'; - } -} diff --git a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionedArtifact.java b/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionedArtifact.java deleted file mode 100644 index d2b702f8..00000000 --- a/versions-impl/src/main/java/org/spongepowered/downloads/versions/worker/readside/model/JpaVersionedArtifact.java +++ /dev/null @@ -1,121 +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.versions.worker.readside.model; - -import org.spongepowered.downloads.versions.server.readside.JpaArtifact; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ForeignKey; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; -import java.io.Serializable; -import java.util.Objects; - -@Entity(name = "GitVersionedArtifact") -@Table(name = "artifact_versions", - schema = "version", - uniqueConstraints = @UniqueConstraint( - columnNames = {"artifact_id", "version"}, - name = "artifact_version_unique_idx") -) -@NamedQueries({ - @NamedQuery( - name = "GitVersionedArtifact.findByCoordinates", - query = - """ - select distinct v from GitVersionedArtifact v - where v.artifact.groupId = :groupId and v.artifact.artifactId = :artifactId and v.version = :version - """ - ) -}) -public class JpaVersionedArtifact implements Serializable { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id", - updatable = false, - nullable = false) - private long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "artifact_id", - foreignKey = @ForeignKey(name = "artifact_versions_artifact_id_fkey"), - nullable = false) - private JpaArtifact artifact; - - @Column(name = "version", - nullable = false) - private String version; - - @OneToOne(targetEntity = JpaVersionChangelog.class, fetch = FetchType.LAZY) - @JoinColumn(name = "id", referencedColumnName = "version_id") - private JpaVersionChangelog changelog; - - public JpaArtifact getArtifact() { - return artifact; - } - - public String getVersion() { - return version; - } - - public JpaVersionChangelog getChangelog() { - return changelog; - } - - public void setChangelog(final JpaVersionChangelog changelog) { - this.changelog = changelog; - changelog.setId(this.id); - changelog.setArtifact(this); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedArtifact that = (JpaVersionedArtifact) o; - return id == that.id && Objects.equals(artifact, that.artifact) && Objects.equals( - version, that.version) && Objects.equals(changelog, that.changelog); - } - - @Override - public int hashCode() { - return Objects.hash(id, artifact, version, changelog); - } -} diff --git a/versions-impl/src/main/resources/META-INF/persistence.xml b/versions-impl/src/main/resources/META-INF/persistence.xml deleted file mode 100644 index 344130a1..00000000 --- a/versions-impl/src/main/resources/META-INF/persistence.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - org.hibernate.jpa.HibernatePersistenceProvider - DefaultDS - org.spongepowered.downloads.versions.server.readside.JpaArtifactVersion - org.spongepowered.downloads.versions.server.readside.JpaArtifact - org.spongepowered.downloads.versions.server.readside.JpaArtifactTag - org.spongepowered.downloads.versions.server.readside.JpaArtifactRegexRecommendation - org.spongepowered.downloads.versions.server.readside.JpaVersionedArtifactAsset - org.spongepowered.downloads.versions.worker.readside.model.JpaVersionedArtifact - org.spongepowered.downloads.versions.worker.readside.model.JpaVersionChangelog - true - - - - - - - - - diff --git a/versions-impl/src/main/resources/application.conf b/versions-impl/src/main/resources/application.conf deleted file mode 100644 index 4e842b41..00000000 --- a/versions-impl/src/main/resources/application.conf +++ /dev/null @@ -1,42 +0,0 @@ -play.modules.enabled = ${play.modules.enabled} [ - org.spongepowered.downloads.versions.server.VersionsModule, - org.spongepowered.downloads.versions.worker.WorkerModule -] - -db.default { - driver = "org.postgresql.Driver" - url = "jdbc:postgresql://localhost:5432/default" - url = ${?POSTGRES_URL} - username = admin - username = ${?POSTGRES_USERNAME} - password = password - password = ${?POSTGRES_PASSWORD} -} - -jdbc-defaults.slick.profile = "slick.jdbc.PostgresProfile$" - -lagom.persistence.jpa { - # This must match the name in persistence.xml - persistence-unit = "default" -} -akka.serialization.jackson { - jackson-modules += "io.vavr.jackson.datatype.VavrModule" -} -akka.actor.allow-java-serialization = true - -play.http.parser.maxMemoryBuffer = 200k - -akka { - extensions = ${akka.extensions} [ - "org.spongepowered.downloads.versions.worker.VersionExtension" - ] -} - -play.filters.disabled += "play.filters.csrf.CSRFFilter" - -play.filters.enabled += "play.filters.cors.CORSFilter" -play.filters.cors { - pathPrefixes = ["/versions"] - allowedHttpMethods = ["GET", "POST", "PATCH"] - preflightMaxAge = 3 days -} diff --git a/versions-impl/src/main/resources/logback.xml b/versions-impl/src/main/resources/logback.xml deleted file mode 100644 index dbe42030..00000000 --- a/versions-impl/src/main/resources/logback.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - System.out - - %date{hh:MM:ss.SSS} [%level] [%thread] [%logger{5}/%marker] - %coloredLevel %msg%n - - - - - 8192 - true - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/versions-impl/src/main/resources/reference.conf b/versions-impl/src/main/resources/reference.conf deleted file mode 100644 index ec0e520b..00000000 --- a/versions-impl/src/main/resources/reference.conf +++ /dev/null @@ -1,8 +0,0 @@ -akka.cluster.roles = ${akka.cluster.roles} ["asset-fetcher", "file-extractor", "refresher", "commit-resolver"] -systemofadownload.versions { - commit-fetch { - pool-size = 8 - parallelism = 4 - timeout = "1h" - } -} diff --git a/versions-impl/src/test/java/org/spongepowered/downloads/test/server/collection/VersionedArtifactAggregateTest.java b/versions-impl/src/test/java/org/spongepowered/downloads/test/server/collection/VersionedArtifactAggregateTest.java deleted file mode 100644 index bdcbeb20..00000000 --- a/versions-impl/src/test/java/org/spongepowered/downloads/test/server/collection/VersionedArtifactAggregateTest.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.spongepowered.downloads.test.server.collection; - - -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.control.Option; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.Test; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.versions.server.domain.ACEvent; -import org.spongepowered.downloads.versions.server.domain.State; - -import java.net.URISyntaxException; -import java.net.URL; -import java.util.Optional; - -public class VersionedArtifactAggregateTest { - - @Test - public void emptyStateTransition() { - final State empty = State.empty(); - Assertions.assertFalse(empty.isRegistered()); - } - - @Test - public void emptyStateRegistration() { - final State example = State.empty().register( - new ACEvent.ArtifactCoordinatesUpdated(new ArtifactCoordinates("com.example", "example"))); - - Assertions.assertTrue(example.isRegistered()); - } - - @Test - public void stateRegistrationWithVersion() throws URISyntaxException { - final ArtifactCoordinates exampleCoordinates = new ArtifactCoordinates("com.example", "example"); - State.ACState example = State.empty().register(new ACEvent.ArtifactCoordinatesUpdated(exampleCoordinates)); - final var exampleVersion = exampleCoordinates.version("0.0.1"); - final URL exampleJar = this.getClass().getClassLoader().getResource("test-jar.jar"); - Assumptions.assumeTrue(exampleJar != null); - Assertions.assertNotNull(exampleJar, "Example jar is missing, needed for various tests"); - final var exampleArtifact = new Artifact(Optional.of("universal"), exampleJar.toURI(), "foo", "bar", ".jar"); - example = example.withAddedArtifacts(exampleVersion, List.of(exampleArtifact)); - Assertions.assertFalse(example.versionedArtifacts().isEmpty(), "Should have a new versioned artifact"); - final Option> artifacts = example.versionedArtifacts().get(exampleVersion.version); - Assertions.assertFalse(artifacts.isEmpty(), "The artifact list for " + exampleVersion.version + " should be non-empty"); - Assertions.assertEquals(artifacts.get(), List.of(exampleArtifact), "List should be equal"); - - final var exampleNoClassifier = new Artifact(Optional.empty(), exampleJar.toURI(), "foo", "bar", ".jar"); - example = example.withAddedArtifacts(exampleVersion, List.of(exampleNoClassifier)); - Assertions.assertFalse(example.versionedArtifacts().isEmpty(), "Should have a new versioned artifact"); - final Option> newArtifacts = example.versionedArtifacts().get(exampleVersion.version); - Assertions.assertFalse(newArtifacts.isEmpty(), "The artifact list for " + exampleVersion.version + " should be non-empty"); - Assertions.assertEquals(newArtifacts.get(), List.of(exampleArtifact, exampleNoClassifier), "List should be equal"); - } - - @Test - public void stateReordering() { - final ArtifactCoordinates exampleCoordinates = new ArtifactCoordinates("com.example", "example"); - State.ACState example = State.empty().register(new ACEvent.ArtifactCoordinatesUpdated(exampleCoordinates)); - final var acEvents = example.addVersion(exampleCoordinates.version("0.0.1")); - Assertions.assertEquals(acEvents.size(), 1, "Should have one event"); - final var zero1 = example.withVersion("0.0.1"); - final var newEvents = zero1.addVersion(exampleCoordinates.version("0.0.2")); - Assertions.assertEquals(newEvents.size(), 1, "0.0.2 should be the only new event"); - final var zero3 = zero1.withVersion("0.0.3"); - final var addingZero2 = zero3.addVersion(exampleCoordinates.version("0.0.2")); - Assertions.assertEquals(2, addingZero2.size(), "Should have 1 event"); - Assertions.assertEquals(new ACEvent.ArtifactVersionRegistered(exampleCoordinates.version("0.0.2"), 1), addingZero2.get(0),"Should have the new event"); - Assertions.assertEquals(new ACEvent.ArtifactVersionsResorted(exampleCoordinates, HashMap.of("0.0.2", 1, "0.0.3", 2)), addingZero2.get(1),"Should have the new event"); - } - -} diff --git a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/CommitExtractorTest.java b/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/CommitExtractorTest.java deleted file mode 100644 index 52fc46ac..00000000 --- a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/CommitExtractorTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.spongepowered.downloads.test.versions.worker; - - -import akka.actor.testkit.typed.javadsl.TestKitJunitResource; -import akka.actor.testkit.typed.javadsl.TestProbe; -import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.worker.actor.artifacts.CommitExtractor; -import org.spongepowered.downloads.versions.worker.actor.artifacts.PotentiallyUsableAsset; - -import java.net.URISyntaxException; - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -public class CommitExtractorTest { - - public static final TestKitJunitResource testkit = new TestKitJunitResource(EventSourcedBehaviorTestKit.config()); - - @Test - public void VerifyCommitExtraction() throws URISyntaxException { - final TestProbe replyTo = testkit.createTestProbe(); - final var extractor = testkit.spawn(CommitExtractor.extractCommitFromAssets()); - final var coordinates = MavenCoordinates.parse("org.spongepowered.downloads:test-bin:0.0.1"); - - final var testJarPath = this.getClass().getClassLoader().getResource("test-jar.jar"); - Assertions.assertNotNull(testJarPath, "test-jar.jar is null"); - final var asset = new PotentiallyUsableAsset(coordinates, ".jar", testJarPath.toURI()); - extractor.tell(new CommitExtractor.AttemptFileCommit(asset, replyTo.ref())); - - replyTo.expectMessage(new CommitExtractor.DiscoveredCommitFromFile("d838fee5d8e834ba9fd4d1c4fe0f8214d6dc90fc", asset)); - } - - @Test - public void InvalidCommitFailure() throws URISyntaxException { - final TestProbe replyTo = testkit.createTestProbe(); - final var extractor = testkit.spawn(CommitExtractor.extractCommitFromAssets()); - final var coordinates = MavenCoordinates.parse("org.spongepowered.downloads:test-bin:0.0.1"); - final var testJarPath = this.getClass().getClassLoader().getResource("bad-commit-test-jar.jar"); - Assertions.assertNotNull(testJarPath, "bad-commit-test-jar.jar is null"); - final var asset = new PotentiallyUsableAsset(coordinates, ".jar", testJarPath.toURI()); - extractor.tell(new CommitExtractor.AttemptFileCommit(asset, replyTo.ref())); - - replyTo.expectMessageClass(CommitExtractor.NoCommitsFoundForFile.class); - } - - @Test - public void NoCommitFailure() throws URISyntaxException { - final TestProbe replyTo = testkit.createTestProbe(); - final var extractor = testkit.spawn(CommitExtractor.extractCommitFromAssets()); - final var coordinates = MavenCoordinates.parse("org.spongepowered.downloads:test-bin:0.0.1"); - final var testJarPath = this.getClass().getClassLoader().getResource("no-commit-test-jar.jar"); - Assertions.assertNotNull(testJarPath, "no-commit-test-jar.jar is null"); - final var asset = new PotentiallyUsableAsset(coordinates, ".jar", testJarPath.toURI()); - extractor.tell(new CommitExtractor.AttemptFileCommit(asset, replyTo.ref())); - - replyTo.expectMessageClass(CommitExtractor.NoCommitsFoundForFile.class); - } - -} diff --git a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/FileCollectionOperatorTest.java b/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/FileCollectionOperatorTest.java deleted file mode 100644 index 2bb12db0..00000000 --- a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/FileCollectionOperatorTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.spongepowered.downloads.test.versions.worker; - -import akka.actor.testkit.typed.javadsl.FishingOutcomes; -import akka.actor.testkit.typed.javadsl.TestKitJunitResource; -import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import io.vavr.collection.List; -import org.junit.jupiter.api.AfterEach; -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.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.worker.actor.artifacts.CommitExtractor; -import org.spongepowered.downloads.versions.worker.actor.artifacts.FileCollectionOperator; -import org.spongepowered.downloads.versions.worker.actor.artifacts.PotentiallyUsableAsset; - -import java.io.File; -import java.net.URISyntaxException; -import java.time.Duration; - -public class FileCollectionOperatorTest { - - private TestKitJunitResource testKit; - - private static Config config; - - @BeforeAll - public static void setupConfig() { - final var resource = VersionedArtifactEntityTest.class.getClassLoader().getResource("application-test.conf"); - Assertions.assertNotNull(resource); - final var file = new File(resource.getFile()); - config = ConfigFactory.parseFile(file); - } - - @BeforeEach - public void setup() { - this.testKit = new TestKitJunitResource(config.resolve() // Resolve the config first - .withFallback(EventSourcedBehaviorTestKit.config())); - } - - @AfterEach - public void teardown() { - testKit.system().terminate(); - } - - @Test - public void testKickoffFileCollection() throws URISyntaxException { - final var commandProbe = this.testKit.createTestProbe(); - final var responseProbe = this.testKit.createTestProbe(); - final var requestBehavior = FileCollectionOperator.scanJarFilesForCommit( - commandProbe.ref(), responseProbe.ref()); - final var testJarPath = this.getClass().getClassLoader().getResource("test-jar.jar"); - Assertions.assertNotNull(testJarPath, "test-jar.jar is missing"); - final var spawn = this.testKit.spawn(requestBehavior); - final var coordinates = MavenCoordinates.parse("com.example:example:1.0.0"); - final var asset = new PotentiallyUsableAsset( - coordinates, ".jar", testJarPath.toURI()); - spawn.tell(new FileCollectionOperator.TryFindingCommitForFiles(List.of(asset), coordinates)); - commandProbe.fishForMessage(Duration.ofSeconds(10), msg -> { - if (!(msg instanceof CommitExtractor.AttemptFileCommit afc)) { - return FishingOutcomes.fail("got a different command"); - } - afc.ref().tell(new CommitExtractor.NoCommitsFoundForFile(afc.asset())); - return FishingOutcomes.complete(); - }); - responseProbe.fishForMessage(Duration.ofSeconds(10), msg -> { - if (!(msg instanceof CommitExtractor.NoCommitsFoundForFile)) { - return FishingOutcomes.fail("got a different response than test expected"); - } - return FishingOutcomes.complete(); - }); - } -} diff --git a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/VersionedArtifactEntityTest.java b/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/VersionedArtifactEntityTest.java deleted file mode 100644 index 470577ce..00000000 --- a/versions-impl/src/test/java/org/spongepowered/downloads/test/versions/worker/VersionedArtifactEntityTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package org.spongepowered.downloads.test.versions.worker; - -import akka.Done; -import akka.actor.testkit.typed.javadsl.FishingOutcomes; -import akka.actor.testkit.typed.javadsl.TestKitJunitResource; -import akka.actor.testkit.typed.javadsl.TestProbe; -import akka.actor.typed.receptionist.Receptionist; -import akka.cluster.sharding.typed.javadsl.ClusterSharding; -import akka.cluster.sharding.typed.javadsl.EntityContext; -import akka.persistence.testkit.javadsl.EventSourcedBehaviorTestKit; -import com.typesafe.config.Config; -import com.typesafe.config.ConfigFactory; -import io.vavr.collection.List; -import org.junit.jupiter.api.AfterEach; -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.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.worker.actor.artifacts.FileCollectionOperator; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.ArtifactEvent; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.ArtifactState; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactCommand; -import org.spongepowered.downloads.versions.worker.domain.versionedartifact.VersionedArtifactEntity; - -import java.io.File; -import java.net.URISyntaxException; -import java.time.Duration; -import java.util.Optional; - -public class VersionedArtifactEntityTest { - - private TestKitJunitResource testKit; - private EventSourcedBehaviorTestKit eventSourcedTestKit; - - private static Config config; - private TestProbe fileOperatorProbe; - - @BeforeAll - public static void setupConfig() { - final var resource = VersionedArtifactEntityTest.class.getClassLoader().getResource("application-test.conf"); - Assertions.assertNotNull(resource); - final var file = new File(resource.getFile()); - config = ConfigFactory.parseFile(file); - } - - @BeforeEach - public void setup() { - this.testKit = new TestKitJunitResource(config.resolve() // Resolve the config first - .withFallback(EventSourcedBehaviorTestKit.config())); - testKit.system().log().info("Starting test"); - final var probe = this.testKit.createTestProbe(ClusterSharding.ShardCommand.class); - - final var ctx = new EntityContext<>( - VersionedArtifactEntity.ENTITY_TYPE_KEY, - "com.example:example:1.0.0", - probe.ref() - ); - this.fileOperatorProbe = this.testKit.createTestProbe(); - this.testKit.system().receptionist().tell(Receptionist.register(FileCollectionOperator.KEY, fileOperatorProbe.ref())); - - this.eventSourcedTestKit = - EventSourcedBehaviorTestKit.create(testKit.system(), VersionedArtifactEntity.create(ctx)); - } - - @AfterEach - public void teardown() { - testKit.system().log().info("Finishing test"); - testKit.system().terminate(); - } - - @Test - public void testEmptyState() { - final var state = this.eventSourcedTestKit.getState(); - Assertions.assertTrue(state.artifacts().isEmpty()); - Assertions.assertTrue(state.repositories().isEmpty()); - Assertions.assertFalse(state.needsArtifactScan()); - Assertions.assertFalse(state.needsCommitResolution()); - } - - @Test - public void testRegistration() { - final var state = this.eventSourcedTestKit - .runCommand(ref -> new VersionedArtifactCommand.Register( - MavenCoordinates.parse("com.example:example:1.0.0"), ref) - ) - .state(); - Assertions.assertTrue(state.artifacts().isEmpty()); - Assertions.assertTrue(state.repositories().isEmpty()); - Assertions.assertFalse(state.needsArtifactScan()); - Assertions.assertFalse(state.needsCommitResolution()); - } - - @Test - public void testAssetRegistration() throws URISyntaxException { - final var coordinates = MavenCoordinates.parse("com.example:example:1.0.0"); - this.eventSourcedTestKit - .runCommand(ref -> new VersionedArtifactCommand.Register( - coordinates, ref) - ) - .state(); - final var testJarPath = this.getClass().getClassLoader().getResource("test-jar.jar"); - Assertions.assertNotNull(testJarPath, "test-jar.jar isn't available, should be checked in"); - final var downloadUrl = testJarPath.toURI(); - final var artifact = new Artifact(Optional.empty(), downloadUrl, "test-jar.jar", "jar", "jar"); - final var newState = this.eventSourcedTestKit - .runCommand(ref -> new VersionedArtifactCommand.AddAssets(coordinates, List.of(artifact), ref)) - .state(); - this.fileOperatorProbe.fishForMessage(Duration.ofSeconds(10), r -> { - if (!(r instanceof FileCollectionOperator.TryFindingCommitForFiles tfcf)) { - return FishingOutcomes.fail("expected a try to commit"); - } - final var files = tfcf.files(); - if (files.size() != 1) { - return FishingOutcomes.fail("Expected 1 file only"); - } - final var file = files.get(0); - if (!file.downloadURL().equals(downloadUrl)) { - return FishingOutcomes.fail("Expected the same download url"); - } - return FishingOutcomes.complete(); - }); - Assertions.assertFalse(newState.artifacts().isEmpty()); - Assertions.assertTrue(newState.repositories().isEmpty()); - Assertions.assertTrue(newState.needsArtifactScan()); - Assertions.assertFalse(newState.needsCommitResolution()); - } - -} - diff --git a/versions-impl/src/test/java/org/spongepowered/downloads/versions/RegexValidations.java b/versions-impl/src/test/java/org/spongepowered/downloads/versions/RegexValidations.java deleted file mode 100644 index b2c16cb7..00000000 --- a/versions-impl/src/test/java/org/spongepowered/downloads/versions/RegexValidations.java +++ /dev/null @@ -1,64 +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.versions; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.vavr.collection.List; -import io.vavr.control.Try; -import org.junit.jupiter.api.Test; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class RegexValidations { - - @Test - public void ValidateTagVersionRegistration() { - final var patternRegex = Pattern.compile( - "^\\d\\.\\d{1,2}(\\.\\d{1,2})?(-((rc)|(pre))\\d)?-(\\d{1,2}\\.\\d{1,2})\\.\\d$" - ); - final var valids = List.of("1.12.2-7.3.0", "1.16.5-8.0.0", "1.9-4.1.0"); - final var invalids = List.of("1.12.2-7.3.0-RC1723", "1.16.5-8.0.0-RC495", "1.12.2-2838-7.3.1-RC3482"); - final var regex = Try.of(() -> patternRegex); - final var validSuccess = valids - .map(valid -> regex.map(pattern -> pattern.matcher(valid)) - .mapTry(Matcher::find).getOrElse(false) - ) - .filter(b -> !b) - .isEmpty(); - final var invalidSuccess = invalids - .map(invalid -> regex.map(pattern -> pattern.matcher(invalid)) - .mapTry(Matcher::find) - .getOrElse(() -> true) - ) - .filter(b -> !b) - .isEmpty(); - - assertTrue(validSuccess); - assertFalse(invalidSuccess); - } -} diff --git a/versions-impl/src/test/resources/application-test.conf b/versions-impl/src/test/resources/application-test.conf deleted file mode 100644 index 5da7a5df..00000000 --- a/versions-impl/src/test/resources/application-test.conf +++ /dev/null @@ -1,11 +0,0 @@ -akka.serialization.jackson { - # The Jackson JSON serializer will register these modules. - jackson-modules += "akka.serialization.jackson.AkkaJacksonModule" - jackson-modules += "akka.serialization.jackson.AkkaTypedJacksonModule" - # AkkaStreamsModule optionally included if akka-streams is in classpath - jackson-modules += "akka.serialization.jackson.AkkaStreamJacksonModule" - jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule" - jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module" - jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule" - jackson-modules += "io.vavr.jackson.datatype.VavrModule" -} diff --git a/versions-impl/src/test/resources/bad-commit-test-jar.jar b/versions-impl/src/test/resources/bad-commit-test-jar.jar deleted file mode 100644 index f6c8294b9e453e3c47d67330615d33e0ff676186..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 359 zcmWIWW@Zs#;Nak3u-RE2%zy+q8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gtA{z_x69U)*nQ*O0J`C_?Wdo^V N0>WA#eFel}008IxP)q;- diff --git a/versions-impl/src/test/resources/manifest b/versions-impl/src/test/resources/manifest deleted file mode 100644 index 919c25c6..00000000 --- a/versions-impl/src/test/resources/manifest +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Git-Commit: d838fee5d8e834ba9fd4d1c4fe0f8214d6dc90fc - diff --git a/versions-impl/src/test/resources/no-commit-test-jar.jar b/versions-impl/src/test/resources/no-commit-test-jar.jar deleted file mode 100644 index 0552202ff560a57415d1c517485db7b38a3945ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 339 zcmWIWW@Zs#;Nak32-#U4%zy+q8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gy$z%bti&NtyO6>r>IkB1W(i9B(kqa|Bx80K@^_j7%a7s6K!> s599+>0Cy6|Kj>PKjRm~0p06A4nT>ph6a*M9%x$SpxWdRJ_Vt#p^y4S5`tHOQbxv9K;KBXZ zzZ<@LbQ^5Vo4>_$<>{2B&rx@m-rhM)sn&>t+2EpJV?xJz6C*bsOQXs*=Z=a)HdW`M z&Pp%!%UFNEzPRij>*n20x3B3v!}|Kx)8KDWnO~VfZvGU!zNZN2qB0;3@MdHZVLyjhY`RQ$b@S}@_v9fD;r1^6A;z{>5m`|0|1|^WuyQA diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/VersionsQueryService.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/VersionsQueryService.java deleted file mode 100644 index 6de09b6e..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/VersionsQueryService.java +++ /dev/null @@ -1,70 +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.versions.query.api; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.Descriptor; -import com.lightbend.lagom.javadsl.api.Service; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.transport.Method; -import org.spongepowered.downloads.versions.query.api.models.QueryVersions; - -import java.util.Optional; - -public interface VersionsQueryService extends Service { - - ServiceCall artifactVersions( - String groupId, String artifactId, Optional tags, Optional limit, - Optional offset, Optional recommended - ); - - ServiceCall latestArtifact( - String groupId, String artifactId, Optional tags, Optional recommended - ); - - ServiceCall versionDetails( - String groupId, String artifactId, String version - ); - - @Override - default Descriptor descriptor() { - return Service.named("version-query") - .withCalls( - Service.restCall( - Method.GET, "/versions-query/groups/:groupId/artifacts/:artifactId/versions?tags&limit&offset&recommended", - this::artifactVersions - ), - Service.restCall( - Method.GET, "/versions-query/groups/:groupId/artifacts/:artifactId/versions/:version", - this::versionDetails - ), - Service.restCall( - Method.GET, "/versions-query/groups/:groupId/artifacts/:artifactId/latest?tags&recommended", - this::latestArtifact - ) - ) - .withAutoAcl(true); - } -} diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryLatest.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryLatest.java deleted file mode 100644 index c0715262..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryLatest.java +++ /dev/null @@ -1,46 +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.versions.query.api.models; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.util.Optional; - -public interface QueryLatest { - - @JsonSerialize - record VersionInfo(MavenCoordinates coordinates, - List assets, - Map tagValues, - Optional commit, - boolean recommended - ) { - } - -} diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryVersions.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryVersions.java deleted file mode 100644 index ec03727a..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/QueryVersions.java +++ /dev/null @@ -1,57 +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.versions.query.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import java.util.Optional; - -public interface QueryVersions { - - @JsonSerialize - record VersionInfo(@JsonProperty Map artifacts, int offset, int limit, int size) { - - @JsonCreator - public VersionInfo { - } - } - - @JsonSerialize - record VersionDetails( - @JsonProperty("coordinates") MavenCoordinates coordinates, - @JsonProperty("commit") Optional commit, - @JsonProperty("assets") List components, - @JsonProperty("tags") Map tagValues, - @JsonProperty("recommended") boolean recommended - ) { - } - -} diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/TagCollection.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/TagCollection.java deleted file mode 100644 index ee04816c..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/TagCollection.java +++ /dev/null @@ -1,35 +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.versions.query.api.models; - -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import io.vavr.collection.Map; - -@JsonSerialize -public record TagCollection( - Map tagValues, - boolean recommended -) { -} diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedChangelog.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedChangelog.java deleted file mode 100644 index 7a1b714b..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedChangelog.java +++ /dev/null @@ -1,65 +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.versions.query.api.models; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import io.vavr.collection.List; - -import java.net.URI; - -@JsonDeserialize -public final record VersionedChangelog( - List commits, - @JsonInclude(JsonInclude.Include.NON_DEFAULT) boolean processing -) { - - @JsonCreator - public VersionedChangelog { - } - - @JsonDeserialize - public final record IndexedCommit( - VersionedCommit commit, - List submoduleCommits - ) { - @JsonCreator - public IndexedCommit { - } - } - - @JsonDeserialize - public final record Submodule( - String name, - URI gitRepository, - List commits - ) { - @JsonCreator - public Submodule { - } - } - -} diff --git a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedCommit.java b/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedCommit.java deleted file mode 100644 index 6f11529b..00000000 --- a/versions-query-api/src/main/java/org/spongepowered/downloads/versions/query/api/models/VersionedCommit.java +++ /dev/null @@ -1,69 +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.versions.query.api.models; - - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import java.net.URI; -import java.time.ZonedDateTime; - -@JsonDeserialize -public record VersionedCommit( - String message, - String body, - String sha, - Author author, - Commiter commiter, - URI link, - ZonedDateTime commitDate -) { - - @JsonCreator - public VersionedCommit { - } - - @JsonDeserialize - public record Author( - String name, - String email - ) { - @JsonCreator - public Author { - } - } - - @JsonDeserialize - public record Commiter( - String name, - String email - ) { - @JsonCreator - public Commiter { - } - } -} - diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryModule.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryModule.java deleted file mode 100644 index 6b75f6be..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryModule.java +++ /dev/null @@ -1,37 +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.versions.query.impl; - -import com.google.inject.AbstractModule; -import com.lightbend.lagom.javadsl.server.ServiceGuiceSupport; -import org.spongepowered.downloads.versions.query.api.VersionsQueryService; - -public class VersionQueryModule extends AbstractModule implements ServiceGuiceSupport { - - @Override - protected void configure() { - this.bindService(VersionsQueryService.class, VersionQueryServiceImpl.class); - } -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java deleted file mode 100644 index 1a4580ec..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/VersionQueryServiceImpl.java +++ /dev/null @@ -1,324 +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.versions.query.impl; - -import akka.NotUsed; -import com.lightbend.lagom.javadsl.api.ServiceCall; -import com.lightbend.lagom.javadsl.api.deser.ExceptionMessage; -import com.lightbend.lagom.javadsl.api.transport.BadRequest; -import com.lightbend.lagom.javadsl.api.transport.NotFound; -import com.lightbend.lagom.javadsl.api.transport.TransportErrorCode; -import com.lightbend.lagom.javadsl.api.transport.TransportException; -import com.lightbend.lagom.javadsl.persistence.jpa.JpaSession; -import io.vavr.Tuple; -import io.vavr.Tuple2; -import io.vavr.Value; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.apache.maven.artifact.versioning.ComparableVersion; -import org.spongepowered.downloads.artifact.api.ArtifactCoordinates; -import org.spongepowered.downloads.versions.query.api.VersionsQueryService; -import org.spongepowered.downloads.versions.query.api.models.QueryVersions; -import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; -import org.spongepowered.downloads.versions.query.impl.models.JpaTaggedVersion; -import org.spongepowered.downloads.versions.query.impl.models.JpaVersionedArtifactView; - -import javax.inject.Inject; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceException; -import java.util.Comparator; -import java.util.Locale; -import java.util.Optional; -import java.util.regex.Pattern; - -public record VersionQueryServiceImpl(JpaSession session) - implements VersionsQueryService { - public static final Pattern VALID_COORDINATE_PORTION = Pattern.compile("^[\\w.-]+$"); - - @Inject - public VersionQueryServiceImpl { - } - - @Override - public ServiceCall artifactVersions( - final String groupId, - final String artifactId, - final Optional tags, - final Optional limit, - final Optional offset, - final Optional recommended - ) { - return request -> { - validateCoordinates(groupId, artifactId); - return this.session.withTransaction( - t -> { - if (groupId.isBlank() || artifactId.isBlank()) { - throw new NotFound("unknown artifact"); - } - try { - final var query = new VersionQuery(groupId, artifactId, tags, limit, offset, recommended); - - if (query.tags.isEmpty()) { - return getUntaggedVersions(t, query); - } - return getTaggedVersions(t, query); - } catch (PersistenceException e) { - throw new TransportException( - TransportErrorCode.InternalServerError, new ExceptionMessage("Internal Server Error", "")); - } - }); - }; - } - - @Override - public ServiceCall latestArtifact( - final String groupId, - final String artifactId, - final Optional tags, - final Optional recommended - ) { - return request -> { - validateCoordinates(groupId, artifactId); - - return this.session.withTransaction( - t -> { - if (groupId.isBlank() || artifactId.isBlank()) { - throw new NotFound("unknown artifact"); - } - try { - final var query = new VersionQuery(groupId, artifactId, tags, recommended.orElse(false)); - - final var info = query.tags.isEmpty() - ? getUntaggedVersions(t, query) - : getTaggedVersions(t, query); - final var version = info.artifacts().keySet().head(); - final var coordinates = query.coordinates.version(version); - final var artifacts = t.createNamedQuery( - "VersionedArtifactView.findExplicitly", JpaVersionedArtifactView.class) - .setParameter("groupId", coordinates.groupId) - .setParameter("artifactId", coordinates.artifactId) - .setParameter("version", coordinates.version) - .getResultList(); - if (artifacts.isEmpty()) { - throw new NotFound("versioned artifact not found"); - } - final var versionedArtifact = artifacts.get(0); - final Optional commit = versionedArtifact.asVersionedCommit(); - return new QueryVersions.VersionDetails( - coordinates, commit, versionedArtifact.asArtifactList(), - versionedArtifact.getTagValues(), versionedArtifact.isRecommended() - - ); - } catch (PersistenceException e) { - e.printStackTrace(); - throw new TransportException( - TransportErrorCode.InternalServerError, new ExceptionMessage("Internal Server Error", "")); - } - }); - }; - } - - @Override - public ServiceCall versionDetails( - final String groupId, final String artifactId, final String version - ) { - return notUsed -> { - validateCoordinates(groupId, artifactId); - return this.session.withTransaction(em -> { - final var sanitizedGroupId = groupId.toLowerCase(Locale.ROOT).trim(); - final var sanitizedArtifactId = artifactId.toLowerCase(Locale.ROOT).trim(); - final var sanitizedVersion = version.trim(); - return em.createNamedQuery( - "VersionedArtifactView.findFullVersionDetails", JpaVersionedArtifactView.class) - .setParameter("groupId", sanitizedGroupId) - .setParameter("artifactId", sanitizedArtifactId) - .setParameter("version", sanitizedVersion) - .getResultList() - .stream() - .findFirst() - .map(versionView -> { - final var coordinates = versionView.asMavenCoordinates(); - final var assets = versionView.asArtifactList(); - final var tags = versionView.getTagValues(); - final var commit = versionView.asVersionedCommit(); - return new QueryVersions.VersionDetails( - coordinates, commit, assets, tags, versionView.isRecommended()); - }) - .orElseThrow(() -> new NotFound("group or artifact or version not found")); - }); - }; - } - - private static record ParameterizedTag(String tagName, String tagValue) { - } - - private static record VersionQuery( - ArtifactCoordinates coordinates, - int limit, - int offset, - Optional recommended, - List tags) { - - VersionQuery( - String groupId, String artifactId, - Optional tags, - boolean recommended - ) { - this( - new ArtifactCoordinates(groupId.toLowerCase(Locale.ROOT), artifactId.toLowerCase(Locale.ROOT)), - 25, - 0, - Optional.of(recommended), - gatherTags(tags) - ); - } - - VersionQuery( - String groupId, String artifactId, - final Optional tags, - final Optional limitOpt, - final Optional offsetOpt, - final Optional recommended - ) { - this( - new ArtifactCoordinates(groupId.toLowerCase(Locale.ROOT), artifactId.toLowerCase(Locale.ROOT)), - limitOpt.map(l -> Math.min(Math.max(l, 1), 25)).orElse(25), - offsetOpt.map(o -> Math.max(o, 0)).orElse(0), - recommended, - gatherTags(tags) - ); - } - - private static List gatherTags(Optional tags) { - return tags.map(rw -> rw.split(",")) - .map(List::of).orElseGet(List::of) - .map(tag -> tag.split(":")) - .filter(array -> array.length == 2) - .map(array -> new ParameterizedTag(array[0].toLowerCase(Locale.ROOT), array[1].strip())); - } - } - - private static QueryVersions.VersionInfo getUntaggedVersions( - EntityManager em, VersionQuery query - ) { - final int totalCount = query.recommended - .map(isRecommended -> em.createNamedQuery("VersionedArtifactView.recommendedCount", Long.class) - .setParameter("recommended", isRecommended) - ) - .orElseGet(() -> em.createNamedQuery("VersionedArtifactView.count", Long.class)) - .setParameter("groupId", query.coordinates.groupId()) - .setParameter("artifactId", query.coordinates.artifactId()) - .getSingleResult().intValue(); - if (totalCount <= 0) { - throw new NotFound("group or artifact not found"); - } - final var untaggedVersions = query.recommended - .map(isRecommended -> em.createNamedQuery( - "VersionedArtifactView.findByArtifactAndRecommendation", - JpaVersionedArtifactView.class - ) - .setParameter("recommended", isRecommended) - ) - .orElseGet(() -> em.createNamedQuery( - "VersionedArtifactView.findByArtifact", JpaVersionedArtifactView.class - )) - .setParameter("groupId", query.coordinates.groupId()) - .setParameter("artifactId", query.coordinates.artifactId()) - .setMaxResults(query.offset + query.limit) - .getResultList(); - final var mappedByCoordinates = untaggedVersions.stream() - .collect(List.collector()) - .drop(query.offset) - .take(query.limit); - final var versionsWithTags = mappedByCoordinates - .toSortedMap( - Comparator.comparing(ComparableVersion::new).reversed(), - JpaVersionedArtifactView::version, - JpaVersionedArtifactView::asTagCollection - ); - return new QueryVersions.VersionInfo(versionsWithTags, query.offset, query.limit, totalCount); - } - - private static QueryVersions.VersionInfo getTaggedVersions( - EntityManager em, VersionQuery query - ) { - // Otherwise, get the tagged versions that match the given tags - // which is a little advanced, because we'll have to literally gather the versioned values - // that match the tags, then do a shake down - final var map = query.tags.map(tag -> query.recommended.map( - recommended -> em.createNamedQuery( - "TaggedVersion.findMatchingTagValuesAndRecommendation", - JpaTaggedVersion.class - ) - .setParameter("recommended", recommended)) - .orElseGet(() -> em.createNamedQuery( - "TaggedVersion.findAllMatchingTagValues", JpaTaggedVersion.class - )) - .setParameter("groupId", query.coordinates.groupId()) - .setParameter("artifactId", query.coordinates.artifactId()) - .setParameter("tagName", tag.tagName) - .setParameter("tagValue", tag.tagValue + "%") - .getResultStream() - .map(tv -> Tuple.of(tv, Tuple.of(tv.getTagName(), tv.getTagValue()))) - .collect(List.collector()) - ).flatMap(Value::toStream); - if (map.isEmpty()) { - throw new NotFound("group or artifact not found"); - } - var versionedTags = HashMap.>>empty(); - - for (final Tuple2> tagged : map) { - versionedTags = versionedTags.put( - tagged._1.getMavenVersion(), - tagged.map(JpaTaggedVersion::getVersion, HashMap::of), - (o, i) -> Tuple.of(o._1, o._2.merge(i._2)) - ); - } - final var wantedTagNames = query.tags.map(ParameterizedTag::tagName); - final var validatedVersion = versionedTags - .filter((coordinates, tagMap) -> tagMap._2.keySet().containsAll(wantedTagNames)); - final var versionsForQuery = validatedVersion - .toSortedMap(Comparator.comparing(ComparableVersion::new).reversed(), tuple -> tuple._1, tuple -> tuple._2) - .drop(query.offset) - .take(query.limit); - return new QueryVersions.VersionInfo( - versionsForQuery.mapValues(tuple -> tuple._1.asTagCollection()), query.offset, query.limit, - validatedVersion.size() - ); - } - - private static void validateCoordinates(final String groupID, final String artifactID) { - final String sanitizedGroupId = groupID.toLowerCase(Locale.ROOT); - if (!VALID_COORDINATE_PORTION.matcher(sanitizedGroupId).matches()) { - throw new BadRequest("Invalid groupId: " + groupID); - } - final String sanitizedArtifactId = artifactID.toLowerCase(Locale.ROOT); - if (!VALID_COORDINATE_PORTION.matcher(sanitizedArtifactId).matches()) { - throw new BadRequest("Invalid artifactId: " + artifactID); - } - } - -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaTaggedVersion.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaTaggedVersion.java deleted file mode 100644 index 11dddae7..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaTaggedVersion.java +++ /dev/null @@ -1,170 +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.versions.query.impl.models; - -import org.hibernate.annotations.Immutable; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.ManyToOne; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.Table; -import java.io.Serializable; -import java.util.Objects; - -@Entity(name = "TaggedVersion") -@Immutable -@Table(name = "versioned_tags", schema = "version") -@NamedQueries({ - @NamedQuery(name = "TaggedVersion.findAllForVersion", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId and view.mavenArtifactId = :artifactId and view.version = :version - """ - ), - @NamedQuery( - name = "TaggedVersion.findAllMatchingTagValues", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId - and view.mavenArtifactId = :artifactId - and view.tagName = :tagName - and view.tagValue like :tagValue - """ - ), - @NamedQuery( - name = "TaggedVersion.findMatchingTagValuesAndRecommendation", - query = - """ - select view from TaggedVersion view - where view.mavenGroupId = :groupId - and view.mavenArtifactId = :artifactId - and view.tagName = :tagName - and view.tagValue like :tagValue - and (view.versionView.recommended = :recommended or view.versionView.manuallyRecommended = :recommended) - """ - ) -}) -public class JpaTaggedVersion implements Serializable { - - @Id - @Column(name = "version_id", updatable = false) - private long versionId; - - @Id - @Column(updatable = false, name = "artifact_internal_id") - private long artifactId; - - @Id - @Column(name = "maven_group_id", updatable = false) - private String mavenGroupId; - - @Id - @Column(name = "maven_artifact_id", updatable = false) - private String mavenArtifactId; - - @Id - @Column(name = "maven_version", - updatable = false) - private String version; - - @Id - @Column(name = "tag_name", - updatable = false) - private String tagName; - - @Column(name = "tag_value", - updatable = false) - private String tagValue; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "maven_version", - referencedColumnName = "version", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "maven_group_id", - referencedColumnName = "group_id", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "maven_artifact_id", - referencedColumnName = "artifact_id", - nullable = false, - updatable = false, - insertable = false) - }) - private JpaVersionedArtifactView versionView; - - public String getTagName() { - return tagName; - } - - public String getTagValue() { - return tagValue; - } - - public MavenCoordinates asMavenCoordinates() { - return new MavenCoordinates(this.mavenGroupId, this.mavenArtifactId, this.version); - } - - public JpaVersionedArtifactView getVersion() { - return this.versionView; - } - - public String getMavenVersion() { - return this.version; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaTaggedVersion that = (JpaTaggedVersion) o; - return versionId == that.versionId && artifactId == that.artifactId && Objects.equals( - mavenGroupId, that.mavenGroupId) && Objects.equals( - mavenArtifactId, that.mavenArtifactId) && Objects.equals( - version, that.version) && Objects.equals(tagName, that.tagName) && Objects.equals( - tagValue, that.tagValue); - } - - @Override - public int hashCode() { - return Objects.hash(versionId, artifactId, mavenGroupId, mavenArtifactId, version, tagName, tagValue); - } -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedArtifactView.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedArtifactView.java deleted file mode 100644 index 3fe2881c..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedArtifactView.java +++ /dev/null @@ -1,231 +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.versions.query.impl.models; - -import io.vavr.Tuple; -import io.vavr.collection.HashMap; -import io.vavr.collection.List; -import io.vavr.collection.Map; -import org.hibernate.annotations.Immutable; -import org.spongepowered.downloads.artifact.api.Artifact; -import org.spongepowered.downloads.artifact.api.MavenCoordinates; -import org.spongepowered.downloads.versions.query.api.models.TagCollection; -import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; - -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.NamedQueries; -import javax.persistence.NamedQuery; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.net.URI; -import java.util.Comparator; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; - -@Immutable -@Entity(name = "VersionedArtifactView") -@Table(name = "versioned_artifacts", - schema = "version") -@NamedQueries({ - @NamedQuery( - name = "VersionedArtifactView.count", - query = """ - select count(v) from VersionedArtifactView v - where v.groupId = :groupId and v.artifactId = :artifactId - """ - ), - @NamedQuery( - name = "VersionedArtifactView.recommendedCount", - query = """ - select count(v) from VersionedArtifactView v - where v.groupId = :groupId and v.artifactId = :artifactId and v.recommended = :recommended - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findByArtifact", - query = """ - select v from VersionedArtifactView v where v.artifactId = :artifactId and v.groupId = :groupId - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findByArtifactAndRecommendation", - query = """ - select v from VersionedArtifactView v - where v.artifactId = :artifactId and v.groupId = :groupId and (v.recommended = :recommended or v.manuallyRecommended = :recommended) - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findExplicitly", - query = """ - select v from VersionedArtifactView v - left join fetch v.tags - where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version - """ - ), - @NamedQuery( - name = "VersionedArtifactView.findFullVersionDetails", - query = """ - select v from VersionedArtifactView v - left join fetch v.tags - left join fetch v.assets - where v.artifactId = :artifactId and v.groupId = :groupId and v.version = :version - """ - ) -}) -public class JpaVersionedArtifactView implements Serializable { - - @Id - @Column(name = "artifact_id", - updatable = false) - private String artifactId; - - @Id - @Column(name = "group_id", - updatable = false) - private String groupId; - - @Id - @Column(name = "version", - updatable = false) - private String version; - - @Column(name = "recommended") - private boolean recommended; - - @Column(name = "manual_recommendation") - private boolean manuallyRecommended; - - @Column(name = "ordering") - private int ordering; - - @OneToMany( - targetEntity = JpaTaggedVersion.class, - fetch = FetchType.LAZY, - cascade = CascadeType.ALL, - orphanRemoval = true, - mappedBy = "versionView") - private Set tags; - - @OneToMany( - targetEntity = JpaVersionedAsset.class, - cascade = CascadeType.ALL, - fetch = FetchType.LAZY, - orphanRemoval = true, - mappedBy = "versionView" - ) - private Set assets; - - @OneToOne( - targetEntity = JpaVersionedChangelog.class, - mappedBy = "versionView", - fetch = FetchType.LAZY - ) - private JpaVersionedChangelog changelog; - - public Set getTags() { - return tags; - } - - public String version() { - return this.version; - } - - public Map getTagValues() { - final var results = this.getTags(); - final var tuple2Stream = results.stream().map( - taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); - return tuple2Stream - .collect(HashMap.collector()); - } - - public TagCollection asTagCollection() { - final var results = this.getTags(); - final var tuple2Stream = results.stream().map( - taggedVersion -> Tuple.of(taggedVersion.getTagName(), taggedVersion.getTagValue())); - return new TagCollection(tuple2Stream - .collect(HashMap.collector()), this.recommended); - } - - public boolean isRecommended() { - return this.recommended || this.manuallyRecommended; - } - - public MavenCoordinates asMavenCoordinates() { - return new MavenCoordinates(this.groupId + ":" + this.artifactId + ":" + this.version); - } - - Set getAssets() { - return assets; - } - - public List asArtifactList() { - return this.getAssets() - .stream() - .map( - asset -> new Artifact( - Optional.ofNullable(asset.getClassifier()), - URI.create(asset.getDownloadUrl()), - new String(asset.getMd5()), - new String(asset.getSha1()), - asset.getExtension() - ) - ).collect(List.collector()) - .sorted(Comparator.comparing(artifact -> artifact.classifier().orElse(""))); - } - - public Optional asVersionedCommit() { - final var changelog = this.changelog; - if (changelog == null) { - return Optional.empty(); - } - return Optional.of(changelog.getChangelog()); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedArtifactView that = (JpaVersionedArtifactView) o; - return Objects.equals(artifactId, that.artifactId) && Objects.equals( - groupId, that.groupId) && Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(artifactId, groupId, version); - } - -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedAsset.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedAsset.java deleted file mode 100644 index 2e67c520..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedAsset.java +++ /dev/null @@ -1,144 +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.versions.query.impl.models; - -import org.hibernate.annotations.Immutable; -import org.hibernate.annotations.Type; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.Lob; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.util.Arrays; -import java.util.Objects; - -@Immutable -@Entity(name = "VersionedAsset") -@Table(name = "artifact_versioned_assets", - schema = "version") -public class JpaVersionedAsset implements Serializable { - - @Id - @Column(name = "group_id") - private String groupId; - @Id - @Column(name = "artifact_id") - private String artifactId; - - @Id - @Column(name = "version") - private String version; - - @Id - @Column(name = "classifier") - private String classifier; - - @Id - @Column(name = "extension") - private String extension; - - @Column(name = "download_url") - private String downloadUrl; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "md5") - private byte[] md5; - - @Lob - @Type(type = "org.hibernate.type.BinaryType") - @Column(name = "sha1") - private byte[] sha1; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "version", - referencedColumnName = "version", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "group_id", - referencedColumnName = "group_id", - nullable = false, - updatable = false, - insertable = false), - @JoinColumn(name = "artifact_id", - referencedColumnName = "artifact_id", - nullable = false, - updatable = false, - insertable = false) - }) - private JpaVersionedArtifactView versionView; - - public String getClassifier() { - return classifier; - } - - public String getExtension() { - return extension; - } - - public String getDownloadUrl() { - return downloadUrl; - } - - public byte[] getMd5() { - return md5; - } - - public byte[] getSha1() { - return sha1; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedAsset that = (JpaVersionedAsset) o; - return Objects.equals(groupId, that.groupId) && Objects.equals( - artifactId, that.artifactId) && Objects.equals(version, that.version) && Objects.equals( - classifier, that.classifier) && Objects.equals(extension, that.extension) && Objects.equals( - downloadUrl, that.downloadUrl) && Arrays.equals(md5, that.md5) && Arrays.equals( - sha1, that.sha1) && Objects.equals(versionView, that.versionView); - } - - @Override - public int hashCode() { - int result = Objects.hash(groupId, artifactId, version, classifier, extension, downloadUrl, versionView); - result = 31 * result + Arrays.hashCode(md5); - result = 31 * result + Arrays.hashCode(sha1); - return result; - } -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedChangelog.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedChangelog.java deleted file mode 100644 index 90f9acb5..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/JpaVersionedChangelog.java +++ /dev/null @@ -1,140 +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.versions.query.impl.models; - -import com.vladmihalcea.hibernate.type.json.JsonBinaryType; -import org.hibernate.annotations.Immutable; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.TypeDef; -import org.hibernate.annotations.TypeDefs; -import org.spongepowered.downloads.versions.query.api.models.VersionedChangelog; - -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.JoinColumns; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import java.io.Serializable; -import java.net.URL; -import java.util.Objects; - -@Immutable -@Entity(name = "VersionedChangelog") -@Table( - name = "versioned_changelogs", - schema = "version" -) -@TypeDefs({ - @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) -}) -public class JpaVersionedChangelog implements Serializable { - - @Id - @Column(name = "version_id", updatable = false) - private String versionId; - - @Id - @Column(name = "group_id", updatable = false) - private String groupId; - - @Id - @Column(name = "artifact_id", updatable = false) - private String artifactId; - - @OneToOne(fetch = FetchType.LAZY) - @JoinColumns({ - @JoinColumn(name = "version", referencedColumnName = "version", insertable = false, updatable = false), - @JoinColumn(name = "group_id", referencedColumnName = "group_id", insertable = false, updatable = false), - @JoinColumn(name = "artifact_id", referencedColumnName = "artifact_id", insertable = false, updatable = false) - }) - private JpaVersionedArtifactView versionView; - - @Column(name = "commit_sha", nullable = false) - private String sha; - - @Type(type = "jsonb") - @Column(name = "changelog", columnDefinition = "jsonb") - private VersionedChangelog changelog; - - @Column(name = "repo") - private URL repo; - - @Column(name = "branch") - private String branch; - - public String getSha() { - return sha; - } - - public VersionedChangelog getChangelog() { - return changelog; - } - - public URL getRepo() { - return repo; - } - - public String getBranch() { - return branch; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JpaVersionedChangelog that = (JpaVersionedChangelog) o; - return Objects.equals(versionId, that.versionId) && Objects.equals( - groupId, that.groupId) && Objects.equals(artifactId, that.artifactId) && Objects.equals( - versionView, that.versionView) && Objects.equals(sha, that.sha) && Objects.equals( - changelog, that.changelog) && Objects.equals(repo, that.repo) && Objects.equals( - branch, that.branch); - } - - @Override - public int hashCode() { - return Objects.hash(versionId, groupId, artifactId, versionView, sha, changelog, repo, branch); - } - - @Override - public String toString() { - return "JpaVersionedChangelog{" + - "versionId='" + versionId + '\'' + - ", groupId='" + groupId + '\'' + - ", artifactId='" + artifactId + '\'' + - ", versionView=" + versionView + - ", sha='" + sha + '\'' + - ", changelog=" + changelog + - ", repo=" + repo + - ", branch='" + branch + '\'' + - '}'; - } -} diff --git a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/VersionedArtifactID.java b/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/VersionedArtifactID.java deleted file mode 100644 index 8450b0eb..00000000 --- a/versions-query-impl/src/main/java/org/spongepowered/downloads/versions/query/impl/models/VersionedArtifactID.java +++ /dev/null @@ -1,59 +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.versions.query.impl.models; - -import java.io.Serializable; -import java.util.Objects; - -public class VersionedArtifactID implements Serializable { - private String artifactId; - - private String groupId; - - private String version; - - public VersionedArtifactID(final String artifactId, final String groupId, final String version) { - this.artifactId = artifactId; - this.groupId = groupId; - this.version = version; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - VersionedArtifactID that = (VersionedArtifactID) o; - return artifactId.equals(that.artifactId) && groupId.equals(that.groupId) && version.equals(that.version); - } - - @Override - public int hashCode() { - return Objects.hash(artifactId, groupId, version); - } -} diff --git a/versions-query-impl/src/main/resources/META-INF/persistence.xml b/versions-query-impl/src/main/resources/META-INF/persistence.xml deleted file mode 100644 index ba026222..00000000 --- a/versions-query-impl/src/main/resources/META-INF/persistence.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - org.hibernate.jpa.HibernatePersistenceProvider - DefaultDS - org.spongepowered.downloads.versions.query.impl.models.JpaTaggedVersion - org.spongepowered.downloads.versions.query.impl.models.JpaVersionedArtifactView - org.spongepowered.downloads.versions.query.impl.models.JpaVersionedAsset - org.spongepowered.downloads.versions.query.impl.models.JpaVersionedChangelog - true - - - - - - - diff --git a/versions-query-impl/src/main/resources/application.conf b/versions-query-impl/src/main/resources/application.conf deleted file mode 100644 index 6b3bb4ed..00000000 --- a/versions-query-impl/src/main/resources/application.conf +++ /dev/null @@ -1,29 +0,0 @@ -play.modules.enabled += org.spongepowered.downloads.versions.query.impl.VersionQueryModule - -db.default { - driver = "org.postgresql.Driver" - url = "jdbc:postgresql://localhost:5432/default" - url = ${?POSTGRES_URL} - username = admin - username = ${?POSTGRES_USERNAME} - password = password - password = ${?POSTGRES_PASSWORD} -} - -jdbc-defaults.slick.profile = "slick.jdbc.PostgresProfile$" - -lagom.persistence.jpa { - # This must match the name in persistence.xml - persistence-unit = "default" -} - -akka.serialization.jackson { - jackson-modules += "io.vavr.jackson.datatype.VavrModule" -} - -play.filters.enabled += "play.filters.cors.CORSFilter" -play.filters.cors { - pathPrefixes = ["/versions-query"] - allowedHttpMethods = ["GET"] - preflightMaxAge = 3 days -} diff --git a/versions-query-impl/src/main/resources/logback.xml b/versions-query-impl/src/main/resources/logback.xml deleted file mode 100644 index 9ef110a0..00000000 --- a/versions-query-impl/src/main/resources/logback.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - System.out - - %date{hh:MM:ss.SSS} [%level] [%thread] [%logger{5}/%marker] - %coloredLevel %msg MDC: {%mdc}%n - - - - - 8192 - true - - - - - - - - - - - - - - - - - - - - - - - From e3e3bfcc21f13248810a3297b7d298f0062fe3f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 01:12:41 +0000 Subject: [PATCH 5/9] chore(deps): Update Terraform helm to ~> 2.11.0 --- terraform/.terraform.lock.hcl | 26 +++++++++++++------------- terraform/main.tf | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl index 74c64e59..78cd6f03 100644 --- a/terraform/.terraform.lock.hcl +++ b/terraform/.terraform.lock.hcl @@ -23,20 +23,20 @@ provider "registry.terraform.io/cyrilgdn/postgresql" { } provider "registry.terraform.io/hashicorp/helm" { - version = "2.7.0" - constraints = "~> 2.7.0" + version = "2.11.0" + constraints = "~> 2.11.0" hashes = [ - "h1:Iy8d1SJLFfrnUjOkUnS/yoAoK2xT1zhiTfL24D7omt0=", - "h1:Tz6hTlEGdWG8tLgeTLFplq57503bjKbsVJ/XxXjrpgs=", - "h1:VZUEQX1N9sf3oT3GfymlNEaCUVpET4SODks9c+T6TAI=", - "h1:WTVURV8QjqRwdhe35CoY/sgwsgVyouqc3Q2AjC1FMbE=", - "h1:XbB+73RLtFjjt1ihMjg1xRWfcXlMH+ezspUqGRV3n88=", - "h1:YXQgYy5YoqnMgKwlgRmkkUhlSKAX2RMOMujb86ua3jU=", - "h1:ZYFBOYptaaNtAPcy9FXMGQYa5qE8v5Yha/B7/2Zpf+U=", - "h1:bGD6LA5Z5v7+t3c1Wq9Est0H3V4nb4rHfli2mpqdaLw=", - "h1:cQhkO3g5uF9zeOfTzGwAkOIPTSR4Kx76PHjXSVz12SE=", - "h1:q6LrmT829K+t6j3nbGOqn/vWCzzcshWN5lFiNGiKDdU=", - "h1:quavRe9VlwM06DoCgMckuj+5T48g+lfG75pip+iIbFQ=", + "h1:/bxsVBBNaKwLwKapK7mR0ZrE+jhkThD0lfc6rVBD/kU=", + "h1:AOp9vXIM4uT1c/PVwsWTPiLVGlO2SSYrfiirV5rjCMQ=", + "h1:FGGkgKf12zBjPjrD0ANq7EhywWM00PvYYw7OTdT/Kq4=", + "h1:H4+hfjVmw3C6udLoPiAgr5MSg1zPsbT2Zh1Skp2ft2A=", + "h1:SJFhIecChiYJlfGwV5RkJ5ZVnLAuQU6PXENbnCS2Ap0=", + "h1:V6dij/WCsJnkWySf0JfUbIVn3jL3Dsl6FydnIYWI2bo=", + "h1:fBXaroqTJYfzQGrDez0AfdRnXI6BYvijs4doTc2K1KA=", + "h1:kECeVFd7xfWJe/tuN+JzWHsMepFzVNFZLg6BJPyFPeQ=", + "h1:l+2Ni3UyoFRxyvxRblPQQYck1/iFmZKFy/UcI3ZRtjg=", + "h1:m1KbYDhUWeSDhtRbbBcfcknrvx05YkWLCo1VHqyDhSE=", + "h1:zxfRtgpWrVZwjkIBuI+7jc52+u1QBA/k7LQZiCiq3Z8=", ] } diff --git a/terraform/main.tf b/terraform/main.tf index 5d0a612f..cfba9161 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -7,7 +7,7 @@ terraform { } helm = { source = "hashicorp/helm" - version = "~> 2.7.0" + version = "~> 2.11.0" } postgresql = { source = "cyrilgdn/postgresql" From a25f94c8bf52cac91279c095b93c5b8cc0df59c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:37:06 +0000 Subject: [PATCH 6/9] chore(deps): Update dependency org.apache.maven:maven-artifact to v3.9.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 839385ba..7d79db12 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ micronaut = "4.0.5" scala = "2.13" akka = "2.8.3" jackson = "2.15.1" -maven_artifact = "3.8.5" +maven_artifact = "3.9.5" akkaManagementVersion = "1.4.1" akkaProjection = "1.4.2" akkaR2DBC = "1.1.1" From 33bcd24c377fdaddb16e26cffb6a67a4535c5fef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 00:40:00 +0000 Subject: [PATCH 7/9] chore(deps): Update liquibase/liquibase Docker tag to v4.25 --- liquibase/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/liquibase/Dockerfile b/liquibase/Dockerfile index 2206547a..c0021576 100644 --- a/liquibase/Dockerfile +++ b/liquibase/Dockerfile @@ -1,4 +1,4 @@ -FROM liquibase/liquibase:4.10 +FROM liquibase/liquibase:4.25 LABEL MAINTAINER="spongepowered" LABEL author="spongepowered" From 3d40edbfc917544687bdb1017ec7f510122794c5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:15:11 +0000 Subject: [PATCH 8/9] chore(deps): Update patch dependency changes --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d79db12..fa7c3048 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,12 @@ [versions] micronaut = "4.0.5" scala = "2.13" -akka = "2.8.3" -jackson = "2.15.1" +akka = "2.8.5" +jackson = "2.15.3" maven_artifact = "3.9.5" akkaManagementVersion = "1.4.1" akkaProjection = "1.4.2" -akkaR2DBC = "1.1.1" +akkaR2DBC = "1.1.2" vavr = "0.10.4" jakartaValidation = "3.0.2" From a84fddea34a4761df3ec6018e3f1ea58146addf1 Mon Sep 17 00:00:00 2001 From: Gabriel Harris-Rouquette Date: Tue, 14 Nov 2023 13:24:52 +0100 Subject: [PATCH 9/9] chore(deps): Update Gradle 8.4 and Micronaut --- .editorconfig | 3 + README.md | 102 ++++++++++-------- artifacts/gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - build.gradle.kts | 15 ++- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 22 ++-- 9 files changed, 89 insertions(+), 63 deletions(-) delete mode 100644 artifacts/gradle/wrapper/gradle-wrapper.jar delete mode 100644 artifacts/gradle/wrapper/gradle-wrapper.properties diff --git a/.editorconfig b/.editorconfig index 32b9c285..1e81c1be 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,9 @@ ij_formatter_tags_enabled = true ij_smart_tabs = false ij_wrap_on_typing = false +[*.{yml,yaml}] +indent_size = 2 + [*.java] ij_continuation_indent_size = 4 ij_visual_guides = 80, 120 diff --git a/README.md b/README.md index d0f9c983..4793023f 100644 --- a/README.md +++ b/README.md @@ -7,70 +7,86 @@ downloads website. ## Requirements -- Java 16 +- Java 21 (GraalVM optional to build native images) - Docker -- sbt 1.15 - terraform (if you want to deploy to a kubernetes cluster) ## Technologies in use +### Micronaut 4.1.3 -### Framework +- [User Guide](https://docs.micronaut.io/4.1.3/guide/index.html) +- [API Reference](https://docs.micronaut.io/4.1.3/api/index.html) +- [Configuration Reference](https://docs.micronaut.io/4.1.3/guide/configurationreference.html) +- [Micronaut Guides](https://guides.micronaut.io/index.html) +--- -SystemOfADownload (SOAD) is built on [LagomFramework], an opinionated -[Event Source] + [CQRS] architecture framework built on [Akka], and as such relies -on several functional programing paradigms. Lagom as a whole provides enough -to build out several services with semi-automatic service discovery routing -and using [Postgres] as the primary storage database for the Event Journal and -Query side persistence. +- [Shadow Gradle Plugin](https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow) +- [Micronaut Gradle Plugin documentation](https://micronaut-projects.github.io/micronaut-gradle-plugin/latest/) +- [GraalVM Gradle Plugin documentation](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html) +#### Feature test-resources documentation -To learn about the topics, please visit -[Lagom's documentation on concepts](https://www.lagomframework.com/documentation/1.6.x/java/CoreConcepts.html) -that goes at length about how the system works together. +- [Micronaut Test Resources documentation](https://micronaut-projects.github.io/micronaut-test-resources/latest/guide/) -### Containerization Out of the Box -SOAD is targeted at being deployed with either sbt in-development or deployed on a -Kubernetes cluster. +#### Feature r2dbc documentation -### Services -Each service is loosely intended on the desired workload/grouped boundaries of knowledge. +- [Micronaut R2DBC documentation](https://micronaut-projects.github.io/micronaut-r2dbc/latest/guide/) -In a sense, the data -The first three are what effectively being given as model views to exploring a paired -[Sonatype Nexus] repository instance for artifacts and presenting/serving them in a -more user friendlier way by providing git-like changelogs between artifacts. +- [https://r2dbc.io](https://r2dbc.io) -#### ArtifactService -The bread and butter of the shebang. Manages/creates/caches artifacts to knowledge by -[maven coordindates](https://maven.apache.org/pom.html#Maven_Coordinates). Typically, -an `Artifact` is not actually an artifact, but considered a `Component` with several -`Asset`s. Here we have exposed the ability to retrieve an artifact (if registered) and -its known assets along with download url's provided. +#### Feature github-workflow-graal-docker-registry documentation -#### CommitService +- [https://docs.github.com/en/free-pro-team@latest/actions](https://docs.github.com/en/free-pro-team@latest/actions) -This is a little more subtle, but effectively, since each artifact may or may not have a -`Git-Commit` listed in the jar manifest, this service strictly deals with managing registered -repositories, updating them, and pulling the list of commits diffing between two commits. -#### ChangelogService +#### Feature security-ldap documentation -Amalgamation of information between the `ArtifactService` and `CommitService`. This is where -we can store/manage changelogs per artifact regsitered by an entity. +- [Micronaut Security LDAP documentation](https://micronaut-projects.github.io/micronaut-security/latest/guide/index.html#ldap) -#### SonatypeWebhookService -This is the webhook functionality that performs a [Saga]-like series of jobs or units of -work. Because the nature of an artifact being uploaded to Sonatype and "the fact that anything -can and will go wrong", +#### Feature discovery-kubernetes documentation + +- [Micronaut Kubernetes Service Discovery documentation](https://micronaut-projects.github.io/micronaut-kubernetes/latest/guide/#service-discovery) + + +#### Feature micronaut-aot documentation + +- [Micronaut AOT documentation](https://micronaut-projects.github.io/micronaut-aot/latest/guide/) + + +#### Feature liquibase documentation + +- [Micronaut Liquibase Database Migration documentation](https://micronaut-projects.github.io/micronaut-liquibase/latest/guide/index.html) + +- [https://www.liquibase.org/](https://www.liquibase.org/) + + +#### Feature cache-caffeine documentation + +- [Micronaut Caffeine Cache documentation](https://micronaut-projects.github.io/micronaut-cache/latest/guide/index.html) + +- [https://github.com/ben-manes/caffeine](https://github.com/ben-manes/caffeine) + + +#### Feature serialization-jackson documentation + +- [Micronaut Serialization Jackson Core documentation](https://micronaut-projects.github.io/micronaut-serialization/latest/guide/) + + +#### Feature data-r2dbc documentation + +- [Micronaut Data R2DBC documentation](https://micronaut-projects.github.io/micronaut-data/latest/guide/#dbc) + +- [https://r2dbc.io](https://r2dbc.io) + + +#### Feature jdbc-hikari documentation + +- [Micronaut Hikari JDBC Connection Pool documentation](https://micronaut-projects.github.io/micronaut-sql/latest/guide/index.html#jdbc) + -### AuthService -Provides the login end point and generates Json Web Tokens (JWTs) to enable interacting -with select services. It uses lagom-pac4j's [`SecuredService`] to provide authentication -and authorization to other endpoints. It is used to provide internal, LDAP and JWT based -authentication. [LagomFramework]:https://lagomframework.com/ [Event Source]:https://docs.microsoft.com/en-us/azure/architecture/patterns/event-sourcing diff --git a/artifacts/gradle/wrapper/gradle-wrapper.jar b/artifacts/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832f090a2944b7473328c07c9755baa3196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60756 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 delta 36122 zcmY(qQ*@wR6Rn$$ZL4G3wr$(C^~UL#9otUFwrzE6+v#9`dz>-$8UKA=FUx=)eZ;1Or zd~{}NCcVW==2~aNQ2E z`CuaUA4}uu727iJ0V!-;H~aMz1lDHz%8n7{{yDokd(C1tmag30=jUCr9=ds!n{yYkX;R zPI2rG=1~{&b=dnRr>4*vu5rRTE1oe;EEQ3Y*7S{MD4s{600@@fZBfQS1yXYQe)_X9 zWF!2Qg61I8rPN`2OT}5|UzoQ!4e6snbv?A9W9mc#f+_Vt$C~0_8W~0&pvu-5ju15v z%*EJ{Ulk*jk>k%?Ewp$t)^fkm{Vf@BC;U(7c-Ud=NGDjib2RSXc9j^-tpSX%dHb}p zxQb&FHTA*)Kn7fGMtj)olHETj#8OzC_j4}mbanP4V7}JsC|uT!aZWMOW3kC|@e(dfZ~y~V z@_8>n(HBd{$_|}}BN~?jicz-kw^>awsjuDuMxTx{utSV9=zece)Ki2N8gV>72h}Ff zkMLQwu2KSk+Ay`5vuwI<4k-X`T zC3FKuw9J@?izpw9&^bqq9=a2_@FsD#Lefgmr$|E43L{e|teI*t2G?d(E`g*W zZT^~^P9kl*MJPDiCLrDc^KR2)Ag9EcT2FSIT6dkBp8$m8TSk z1*6X3q)^8tbec))-fX&3+Q1#KuiEEXA!WexaCcs{%q4bTM2Q2UjlI~m{i~-E^d2k0 zXQ>D8E&Qibix0q&5$xRqy}|gDp*ai+DJp zq8Kud1-4I?3o8x%yV{@)luLxxop@9I@|&;m7mgyG@4g~?MlXxjshX}|mlYX78cr&r z)9BsWSw2!z4K=&cDguESTuA-C{X~@itg`?7&M|8R%@j#UHEylhJc8(`dU(6mJ6b() zmuKPNGQwqRvB}hVTPiT@KE*7DU-<)P1j+Roo;AV|;oa{*@wajDmBdSDok;f2!3c%e zub;RSe5?GeMHYtl=#I}u-KL@&CZCg1gvqV zagwiuSfdRS)+p2mQE=mlgn9FdqPqk84M-$gzDjxTxgf!l23Uai|2TurDbe<}j*O?* zh_W-{8<^99kENpo9RX2@h=FPfDI|R%7`GIEU`3@FtX1?4y!i2F&dvUZE3uNarPP9y zK=cE#4{`On($c`!HF*gV8|hxU(lDHe@SyP1=;F7qXW zx?Ce#9GClVDCF{Y?gad8{44nVc7_Gw>P2=yw@_xKmBJj#CaDn~N{)l0hhT!U%2gXZ z4Le$?)JZHl!ZSJz;^4fQ>J0UB0=o}VQb7Vc3*Q@v^M(I>UX|eI8DvVW(mqmKSMj9r zk*UJ2Xx3@2%;e=BT)L^y&~I%h?lwyg@1An9UC{k>N097VEKJM!Ym%^H!^<;>L%e3E zCfng|NUtu1I5tBPbUOUnw=iap%-C!7oh(*XVeBMcOaP#l_=5ZZ%&-Bic7K^Jn;02NadkRB9_= z*iAA`t}BeEa6Te#g!b3zm<#Ji@V|SoOXf+rU|s*Pf3V+BtBXT|M`}^}?fA~ckflc; zj9sweaZ{<79Y3jTwB}FheMB%&o$T9V$!Y?mV+`VpHl_K(yA-VaVVl57Oc*5KSq%1t zIAJa{!am`;W+hV`s%ZNV>co36u{scvV@P$%_U6lw79BRCug1g>0Z1G zIs#tFh_f%rt5rV{Tj}ukVwSBt0}3mXf^-^RT0@F)pTfw@q)UK#8kt9K#qX@Xb{!uu zq*hW!C1ekWm`&h_gSKk>7xYJV*yO7hBf|-W$2Nwi+uSleLxhd;;&SX?19KGll^QGd;n6irTXNp+t?F*S zjFgG6Zy@?Ppzd(&OkXw}s=I;~7IpprzDI12Jg77$DVgfOt+X$LANKC#2vV*K7(Byz z1-qnezu~;n{(VPx3`tj$h;%O^H|x=%qYh_j1iXsTLk(^;b;|n+PRr1Jk^0qpnIL`L zSlVNL~27L|5I{gd~B_fTL>NQ=#$a_cJ^B)`LvJYWi(9FFt&Bqp?|BPW32O4fc3;5x` zI^vy}xkgXuI> zo9>CF1S_yn&0Ntr6NcE>OQo1X{Z;1bW0B-GXQDs3vAVF@xK|myujWGz417#qS!KfL5 zPpxv@3W&-=XcC!TvjWDEChH{%3i)$Mm4Sav1n0XA8&eLE!0`7RmLbz!|LdhA$!X4( zJOXA-BvKBq>&d3;4R_9Gz}*pTAg&Eg`r3?MHi zXrUI5nN&+xkdfB8lx7!U-eV}wE`J2T@)oyxGDEDXl6PRn;zj8n9*g-RzVQ@xF)5TA z<&a;@Yv)Z#TI;n-4Ow;7A<~S0{Vy2Sz>SZ+DIy99-}r^V8tpl>6Kv~=Ub9DO!K3bpK&6{au9}md1z?T~RI?^)L za7r#`(RZwV-!EBOfMvb%a8C?_uhm`)vo}WK5WV)Kft%D~7VZ)Fw*x$*`jY)JKB5ta z*L~PB(Tdv{4_YPc$VKfC96Sdw93^@ahN&}+T?zTyjTf*a)E}qGjji%d&F05T$EZpt z@{IioV}s~wtaHpx+7x_gL5*O%qvUk4v1mmosgG%wS;;alekSVVo%*|otc#7MtU`MP zvHc5*5w;6QX-F-aNLRnnP=@9`a!&SuoHp9SbU>TcVVmcsOZ8Rwycsh1%$w5>Hfhm& z3s?IcU@4{eMM8qtJQ;+q6)&Bu!e#=swv32Q(&x5X_f%#f!@o=*YolWHCxK z#08Csf2bzRVt$a%!PsOQiQzPrYS*6m~w~rk=hDS9x#05auP=EBFU|51{v^84U(b~9$k%k{kwzZ3-U+JHJcZd zc})&214p-AW2b9eZAM7O{|BecaiVnEILQXMcd}M+$6Z4=4bl1LoA<4tN_Ugzvgz>D z6cA6#4Z*ASiZ&8#86?pHjY4a!L{3}gV*WV$wZAMQA;kF5BJqV$sa^G^m&y6$7kc2j z<&doyu5A$oJ9!GpRsAL=))?%?Y^B>J8ptiU7_NT5;DVJNm)faxnJql)eDhXhfYAf} z?Hk%#I)iMR9zjJD#Jk;>w9TWFrhp(;5iP6jgQ6Q9;eS+f8)rMD@`=?WS^~D z`geXXA0py{t3OdJ;0Xo#blQ~`#9HC_tE(=< zOp%PdUOU)xac$Yfg;{j+zZ0hEp=bdHf~HxGP;QRo69)mxtJ1ShrrEUwaBE<`FXI7eEqh>)f29GMQt--aWV zMRDgX0`h_AUD0T;+onceaW4;``d#Dz_*j;0{3Bz=cY_eP&oL zC0i>sSk1 zXm{OlrxjOgLynpcS6IL<#{;e{NjIZ&qr>hKMo--kIR}=WaFxJP`eNe9E!K0Ui5_E# z`|VbhC34#q+<_;r1p`{?&V#dL$FTBZL3fOq&@2mF+VD2CTBa!R->>TNAvS-Qqah9x zGnZ~2MJ|1?VjBX#jVWfo!VMVfr8^o}wZ3o?oJ$eAL>|m4cs+$LHYJxQ3 zR}GRjX+~6ax8B-<*OPFGA%pU}vRXLJby8F1zQoz|5^Z7%P@2~)uW+*?zbg<-xEa1N-ipwKSYUQXqT^|7 zx_jj@>zki?DSgm(PK5dA2nG}iiur=B@*mDoUe1)K3CB-Ezd)NiVm0Pt91TD5pgjb_ zI;BXdy1YIeyy+=)nChIdA&MzCX9`JWG9|Ltn!U;Btx)@4Ry#~BQ1h7wHZ@R(%jVid z|3rI!LvRBL4STnv<#(Q#BYqHqWuxm{wRe~sqLPb7bTSc^&U?3VbMy0CTtT+$L2pfM z3cGx@H`Z3Tqe%x?y${Pv3Q92zewt-(=RRx1CN+Wqce*;MmV5T3@I*7lxm@w;`#;x+ z1VV@fMg{H^eQf+AIfr_kL>P2kN1#0Fbw{8JO!rRFF(|sDvpjkEVE_iW10{7yQwYAjm7NYU4}!Ce z%IUK&V$(mpjKk!4td#f|df~URHcG0WIG+Y@x90|S_al=Q`9{U zBo8r{_MBOC4LE7O%Iob7088&ribIFxS)eM_rlEFMk%Z)2UQbDykd~ul7M;tc-*GWR zZG{eD1bh4K#J{KyJcT);##pLkUN_M5%|1dms*l#BUDTGZTX-+FOiU^i5u4T6NV7iT z34&_xQ+d)`zrDabycvLmv5T0jS2zoRO*oaTuQ6?{nhYK%FRELruGtPWrw|fQe0XA( z@jedR^U1D=9)h(JsyEN^N5|#r$+Qr+aLTRd33iPt-!H!a`yo^tA}ff6}-b^&s&cZSzm{gJ?^(Wbmc3*YX=`$(p3z zwv6yrR@D0x?&Dh_TQB4tA^^a(G1RADxh#c{a zWf21R1*&M4uXbE)quQy+Qn0cgyb+1e9oWLnCe~O$q$7Rq$cylXJ$}vbyb~c7D59hE zkoU|TH`pToD-{R7PMV_uIRqTaFjl{49))Y6eY^l@1fVf!=;Q-YJL^OGBli+0WNo-8T$Sg{ekh|vk-4Y(z;F6 zpKfFB1L1?;ZUmgcQ52xFe5>#=#QlR6eNyF&S`ea2J9V(Ne*{WF)|UkT7vBg2P~+rl zc3tR_lwzzh`vsw7Wey=g%62X>v6DHL@Bo*BsiI#ks8gO$bw*CbtCS;;wv*uXtg z-eEN=)t)5=lR$ZP8KRDTO0U`YDA#2#vw2xCojm;4%Yw_|8{sLU-oN~WQ}fA|E?#&f z%HX~J`($%S^W_TV2AH!oD|XsauMt{=dwBF58po9OKgCzP7#R$J==peyCHM0LB36&i z_yOVYllun8uuVv3t#n&hAKhYi#;Ja?{8x)f5_y+D{NS9}9R@J%ir}#7O0KBo;ctD< zEt($PA=gGLMelrxFe*Uw>!b#aHntduwb z44HfONOoL6_JT8jHb`^qzBv#aB~Bo#WswdyWp)&18O1K!W>BGiHwYiny{U4=G5C1L z^>QJOu*54rF5Jio4CJ!NeahOaZ<=ExwX`75w>z%=-U94C%6bB)TEE?OJ}0E1NqsG zL>Vb)in&baxP?EMvWcstelgWZlXk*c{HcS+QSF2V?q`E%RI<(U>zT>cxWdQM3bAtv zp7`drrDKtu4f_7fc1g8}iN{=GQT;@GBSD9n^tcs6^d@QhC5vv^7K4&!3FU9E*8dvA zr2I5L(L?Nbk66K9p0$vKGQsyw7|B1x;jj8{EkL)TLK-qvGG*H16cf=MuIF0)ZxysT zVTD>3vc!h8;UKpTf()s`Y@^IC6UbVs`)^aDOk`%A2Qrr;{peH2|K$$8A=#i46a=Ia z5(I?v|6R8~xv>E?d&Na1^nmM?d1W5_I@q2-_$}BF79r#)Xoh(@?LM>cp?Gt)#$sFP z4HO_;FqARi2WjM9WAA8rUd%}gf&vFMgZ}KK|BUN3|H)&(=hGWppm++o853ziUhg{- zt%*V~i24Ai3<;(3f(Jr|*#|*-`Z`ZjwmTZo_FZ_1YVf zIJGivLkX`ozzD}?i$#5a!~I`inRiWR?w*3-(H!=WkCAerW|%GXfD}Dpf!eP{m`Bt> zHNQS`zeHf>FPrz0(UX$kin?qop3StUd}l$JE%-RrUrf&zq}Yx+cb}9fXnK&*mEz*N zT(WIOCb=E(={a3iyq4=$uldUFzw(ou^iPTGCF8t;e-i_8RVT+x zghn89Bl9zKx{X%{MOtQOtzIBz^3d+|Mlf5fZ)<_)U}IVibM2Rys4JWn%lG5@`3zqY zNMciXMr?|M$`R)S_>ynJ&t61Kk2vEt-3zOzgVA7}Ri+On82@N6h!T5voA5e)-_(RK z<6{0^^Z8tn4zg*1-`z@AE!+OPY$dKNb$AGV&l|pBE>}f8oXpN63LFo5bT@(S^H6VyPtUEWS~P+@YW;uwH_5s&@sf`B);L((~8& z2<(#Bh&$yweX*|`erx=~%E(xUd&D>cVBd5OGf6E5%(TA_Ns`!;BFhD;ef=dw`;HV; z(?>{k6!)D2XN!=fAX=prl&4v!RF=5T9w$r3RfqW2t&=9PzY+cy^9;J)gR&nWAVpvx zAYA_sb0mH#Fl2w6Mjig(9}wJhl$RCBdjdkhx59rm#n-dX)$aoRUdPx)@ z*s7YDnIt_Q`@_+i@#xlPb(28i=P>21p%gf(ydTKV39e3h=qBj`X-i8B%bqt2iw!{l z_=04Lu=K|ctVm8@Nfc2|FCnvV+YBr*)`$o%L^dZrPHLkyIbq*iy$vKD3E>g-@Xi8& zCQC>|RX61oawM%5F+^w=zh-nj&7dW7K|TRM{gO0DNV>e^e>-3hApIdM0u zB2gW^k^c%`n+dQdRBp_}QQCEUT)+a3N;#8nBCQfoD{9N&Pe|0^L)W!em4 zB2|Ic6B!Z03=!euNRS9mPm_Vf{4>Vng8A|mcd&BV*M~-j(-z4LDbc^mRJsRHi=K&e z;jnz)X>zt+*|<%(qqEQZhCi-6n0)wP-x0wIa?N87Dy2U;Me=!gJR(AfKyeTXL%>HM! zp?_I;Y?Mr5(uk-x1#1FD0DXQ)M-@T_$bO-x&rab24^&1&N^* zX?|0f`Zek*)9D-(JOoT}-uT~KOa;6>e~|`?SD#85OGGeWAwVEB@~BOX9~Fdqx67|A z{mC!*&pvC_=iM|?f*mG+Y(BpNwBZNcH=1)>kUZ(X+t=KwSXEv!2i8$~=nouJ5MHhV zi97w#|K@H$`)}B*cMp>8MbACp#AIIR1T3Qn8=*MVT))vb9!2wyvSh{CqdqIO`8KSx z?m?yI^>(O)3EN7bu;)-OAq~|t5$v^0VQZgFquaWTL|6VM|3}z2{7e!FAYb|hNO3*i zOA4*Kk)ls)Dh?@o{*{ew^+c++(E7jSxR|6)JI0e3)Z9RKU&1#Qn`otRs~$>A3E~A{ zvWRFu`a!+zb90HOCgbR3-)qg^a%8oh>+x`(@B_>mOjhf^Rxoa49Ib?|&cxGlFp7At zU-l)XcqcL&l?F2%V*$(pPNx67=V6_y)N9d&&sQy(q@RB)&XGIQwPIXL&W1buHS1;7 z%J(b_F-|b3fMp0PvHC@lOh=lP&JP7hB98vIS7aPYn~iarfYchN&?VoyApzkc-bPh! zkmCMe^8Rq@>*@d4b(CEx=SG)Q$-F2%X|~YFmS&*J8P~W`$pH~cUNx~u+?|qH$XAeX zFIetc(@dnozD24BV!AsyF)MC|zvN`ycx^a|n*;RsT#32^Tn?(!`1ft1xiW;OZVPK> zhQ@!qmRWV#X11=mszNo#N-bsc@~7u-;K6cwt&-xX&CK}L=^G?cJt53B;WA@?V11yq zMNt3MbP^mmxuYd&SZq`9$iAmWXBOEG4Yh={$|RA&{zm*?UaR3p@F9|?#bf}#Hu;lL zWap52<*l54E0X!4P&-+s&b2K#wrW{#+ieet?_|zxtNk#+zMtlNj*}F4WKzk`evjO< z-ZS1CJ3zn}s8e8SEL$Z9OS#3}kOYDv{iRkp8Ve);nRp#^h0j5#kw6MM+u#y0GXiUU7+yi8A&Mx@E9-<%C z;OnfBUoK%i@Ht)-aR>;KE`34C|MBe)!)?3a^FO~}O;63GK!Vc_RtE?;o^|Enrum+g zn*J!R=~{3QUhR0qy`NkYk>JzydWg8+Ijv@rOZKzIh_i7m8akSzglO*>(t`PGGd;oz zwGAf@rmmHG0)4L|alntPJe-_j7MIHtG>{GTJ~MKv5i#->80oAkc=;{5lAgg2pAZYu zQf);d82QWJ&QL?4wrzN5JA)J_FfVmW><8&LSraX#Das<+b16uaVT^0I$@Ladld3)o znm!9&p*3yQs0Tj}I5y;qU#B@VieIA!m%2FAO9B##Q#4nu<`plD49qy6s88x z5RTJf^AxMGMzR7Fw(z~@+7J~4!EDv_C7+$FfcBif>ZLxu14^%$4 zy-=~OY7waE(J0tdhRgSuC#liMA(5B)(wmpcl9m4X8_0&cFtJyn81XEv-+ zCqAryPQge)6osY{X5Qqwqg2k;`zy>Ed#qu59R`_N_COWw3gRrGR<;Ml}FCW?>Cu82m?U7 zd>hMJrN)%rd(s5oQZaQ7PNuP$MIpJwK)Y0pS8~+crS|;4VqEk52lsZN)C(0_cLQx< z<5N`aipemSM9uT%LmGJoOlP#{)WEda zpEbKkvFU(=%xXoPd>>mP8;i?M;e;$^Cu&Z))>NaVsNYpK8cX)oRkivp&Vcz-rTQd8 zCE9DMBdi`_`DqN4D28(5@}@>T3v!v-UVAVKitgI5zBD1}Lb)v%LGgG6QcF14-3%4G zUMe^5YybkpKn(^*Nc$w|{7Te{RX(?w23uG#2Fz9})L<$7=xM_g-f2YYd?#NWRjb^&>=yObBwT+JPe#C^!| z)U84Q#R2NS-a^5O!-EIW2)9^nbIM5mI@iKV7lf9Iks%;V_cNnhp=DB(-+Rt!*`o-%fYNM@ri)r3V>)Be=!mKz>1gA~)0 zWTDEYyGGup35-b8R^?Ug)2SRc4?Yq#5Fn#eIG05c5hDpSS>Edj&CuZrOjLb6$1K8_ z>P;;e`Gb~~DF4a&1~e{GCSFyP=l8vVX$`UbDBF&4?a#;{eFz7o#=V{=%O?rJ9cGM! z&^c2y?2xSF<-wlwKt*AQk%DxKixbP5wqmioR3_JxT(YazOTZtOMRV|GDm|Qb;OzW` zng%73v$K_}r`3s>_=PHYyd|m`6;PR(Q(AWC)i|u+i?C3VK_rCp+8Y*+ zHQ6;9x!ftg!6u@}qPe5OJPFg*%i0Zop2cv~bEy2{vj8b`86NGcSu@|I*tFZ7tb9@5 zln6cF*zXdmj@_^_!CfG!fxI5^2mLns!y5>-IFi5t1Gqq~7ZY9uPYB23jh-viAX+!9 zC;SnEKTDtw7ZWFz0ZwpG(-d$!{tH)4npfr9%@HiZDK?`t8q(70dY*kCkcYcTml11@ z{SMb7*Ti#)wWD-HXNbWdr#eodky0 zfY6q-46HX;v7xdb`j9`a1zDrOvscBSoIi=T_b1>TQHVNdguf=)aUNp6H4t~2l@Yh@ zbBOkk7_uL73|IEu2?M1H>v%r}SFI?*K zmn-K)nSrk9#|P*;Nu7__CXX&U>}N`a9hdL+RHlF`x2QMMoLFX8SxO{YcGNYy1r26^ z=r}%9RR7D1Xl2G>>AYAA+a>RE{xATnZX7J!uP86S!n8l3o92)HqRHX_&Yit!4e?G2 zkWRdl1XVH5MvF~o(lzoC(A<~c@O#QMTUkBXk@h`8+qW1)S8RGk=-05#i3LpxO|iDv zy2P_$9%j}xl1i`A5l`6$)%?hmJNy5t(#Q^W>Y8HXI6!-&0F}2IZ3d!!I?1V;J>%Z!4b&G&Lf|Ue4Vs z4Aw^whQ-7Ah!q#gLnP><|A~j7BKRohu~J1RZ>>ipY0q%_^iRzKNVu?@d55-rKy^XE zb6`A}sCe909yEAK-U$g?iYfWSgUI;!8G#C8FA{e3WWv#lF%Aaxq(j*`XUG)j;$8;1 z4XWzEZ34p&QQWKQKgnD`<+!QXHBVJ`gSJC92vgNERseD}TIPsLb$fB&y!g&wvX45g+lD9=bgnJcT%z9#qhv3-j zV{ULwxx4?hX;Wy4LAQDuX9f;OL)z-!=wlwITKRjt#1Z+=8j&$84%7bf_3Vza3haOwzZe; z$%97^y%2|QHiG~FqUp@Ii2$|h4XgdH`nTt8MI(eof7p6kGXG%dsQxSNhOKw~jy&wJ zt$?n0ma3i$vJO&Ld|A3zwfH0*q^VsYIN0(=h%eW-`?J2?&C!j+reyyZ+doS+<}^FK z?lJ4rux+IevIazAO(&3%AMjOI!?)r4D%^o6?&J{(qj;gfgw88~;_Z-41Gyqvg>Qkhgz|}^rDtzMV;{^beu=w#GLNR>K|Gjk^-S>!a;iG| z3vpr`j3caeM55Uf1G(L-8H;2@O&mZIN#da(Hq8gLttnLzeQYzn_FQ(hQaVW)w1p z;y>kylQ8UvXr{18rC4>YpI8s=xIe0mUG#z(*o=5rSTI(YuU8I)?fR12(7(fDy%5s& zG>iR^Vqc*$oj|8q;7en~qveFEUgs&q*T^i3^v_X}+}G%`kP{Kz#+KJeI7w-ch$-T4 zKdJ42-TF5XUpgu4$4as!-y z(z>CT2XGguGK^PD}pjD>m#xS%=v^J z^1xaHIHWp_8O>Gy`WEFj<8}1W_#>(n7Nb2&VjE|OJ-NRpv8gG7e|Kt+=6$aCI3R0;_{BiV5TO^vp(JTXyS4fJ7lb?w#%Su&Zv|)^?j4JS<$zGb2qbnldJ-b<&I+-XjR}({B7wI)70|kEtxhL$lR-kh9#G4@R>PGLDmcXr&&QYX{{aPZ|J;1g{mFA(}_- zT?~%9C-<@+15r=<*a}~=k;`KI*Y8XLeO4=NH&?I30Yg>+KUMGeEM41nz`Km*Yl_jg zQbq>-;o+FsuU)7OhT_!)CO5|UjBm_8LJM+8>-I1{QndGyPi|SS6Js+LVl^XI8Du`_ z?z|WuuIt6V)|0w&lMaECAuittL#L_ZJGrP)yr%Mr7Chz;@JiJ6=NnuLU6>b&Mfjl; z^EoI5tMaCIM{O}l=Dc(t@G?~4c;l|Hx}pZfIjQRa*&j2}zx$>RjWGpWuO>*-OXf5+ zf7YQL{gt%ix0}5g)(M~P&{+*m!rB`b1ibHvs}<{tnCO)y)!PBeOJN0SQJcX`6?YE9 zN}qMO4#lov?7wRf)<>{(RIyl&&aO0hBt!(Dz*9)+C^)4BE38*ab2 zg}L$!_5PxsKcOmm)|!ATnzzVdahO4DyqC&hiBK(@+8d&7jP6l;OXX8-I=Iz=8dp|h z5~3vNPl?|X2nVIj#7R=U?|OACt@T&Y{G%SHN-+~qyrXZd@fYW6zJcC@8J5r-9PeI5{R~WP))G6h_7d zA!21M@0A`(Q5+q~$|Y(({(GeOtTgK@@)gN#u+Yue<*#bTP5k*8!8$nBlyG!Ldwlzj z=g%VG>+^s-@Zq&KkS`cC?f?xfPlwBKU*rcCvwC3AEbw@i6gKIT*TPivX+f_ye}AHr z-gp}pR;ANpvDXpi4aZ4Ghwg-Ch`39;xlme1?`K*#GzW-Fs7y0sAD~Ubx4(IbGF>u` zOVM%IT#$J8t%@#im9z~En&(Q%v0t7NN%*bH!rps=ODgW*soit)i~n0Mn( zyweQ^0V#r*WDH_7>jsDH9RU_ykD-D`!ed1?N*a+dm5peQRnU|ZA2|js{SEuSZyXIMY8BSY8oJ}$EI(nG*_<~u{mp$s9zhpt)D~yu8 zOO-nI{>y%~CBT0_H2V1KVa||HZWg_U6d;3WVy$^H2;?sl{V7n`EU1b&ShDQEtMp6x zFO(B%8Bdy~kwpuNBbGle_7~bnRPy9!*hkdfZ_oK}(BqwL$3tT?<(GNH4*PvJW$cS6 z0g+7Brg-z7OYr`=F;Ala3YEXs6Sn;}CJU~RI#g`T=iI}WPARun6!^0^cE&r1kbq}B z0A+Elc^G5qdpb%*J9modgc;!!XO`yzbK1+kK5SMQEiEYD==_^PC?1HIV)Ql31_NIa z+x9x@m1`6+56-->BYYB~kN-w6h)R#|9*0sXVXA+XUG${4V)8B62<9pWjX*Ss{+s2$ zK>yl*QETC3<#fViA72(AOI}J;q+kwI#|AnjUjuz%rA3I1Ek%avmqreGyL^kjhjU}l z7lQw71*88wWf^0S+kXm)+`m%RPuq{DLRJsH7t{bZST2I(@pjIaP1l~A&Xdb6%UQq= zbeI0W%xdI|&RjUN@krSCnb}N)v+$^R*H2;Cw4n)e0!=2AezH=4?U3CspEL?dG%b}# zkOwv$^SCk`2Vs?MiY3&pRp*FMRCE5Ra=p@0!!B3S1B)yAYt9|0;T(X7qBQpo+ZB#Iyr{)v{6ba&lDC)eT%8C}7kU zH?*#f!cP)Ujxr;6?Y$ zi_a&~Ffq>g9<2)^Y&tvONv9=}td>Ri zv_M4oF`>akdA%c!i}i8AJ{|lnEI9NOyWh0G05R@?bt<@xDV|MvuZ0lv~L4 zd>WwGZiSx5!oIO@|7MRf?}@mFdz5qZbwZeb)!OcmhgE<7__>5`+9ijzWx?p<*EciC4>}7K znw)*nrno9W;*O;X2a@NK8d7fjzmdCiOIj-QAsE0YFfeP&d0H< zz5WsD4TnHp*#cki3?2x?fl?Yp`uZXG8o76A|5tiJAu7l1C41{6oBxE{@gwU3Rwt?JS>Tlt@#k8@vbdcRs@!!paHfl=ZN zn%js!DLBiNe*R02kvSC3L7L?eonBI5wMQ>`J6NmHnq1jU-k1?)R>jAZxmqN@TYI*< zl^algSS>lwExpx`4>EMeKf|z7u8|oyCp8bWN2kF)NjSkptV;y6u7NQ_?^y}ec`(+6D~F>&<>SP7w*to*_faM3aaVTc>X7(#dt9T;kF*tI%waeL zjre)Sai)ZDJeb_6PWqz=aps$5r?#^nD$@LGz6(z@;L{t=3dbU9M?=k8wfav zmK$CWN3KHbT;MBeO%$WjWH=4qbw9D=u5;&FnCB9u!bK}*sY!eJxw3Ul~u&_8V57}4_D_H+X=8h``UK9wQbwBZQE}DTidp6+qT_px3=xK_r9CFIMh3UYvn}Dk7B#j) zM@$7Kr$>E$xF*FTk}2Mlfc*}vZ4O)DAhpCOXo6|6dwY!h58JBRVV*H&+Tyc(-ByVI%myd<%f}t48z{XjI!^ zsf53X#{T%D2x)4V#JQ&#Y|jhTy2cG8bcLb90w(xtjdy+ z>}L$jH+15JVt>`?$d)NO@grTg2ntD-Q?aCS@G?$C>Ddod^9iHdqwUW(Xj~8{t~Dd0 z6a>+ThPfrI?LV_s8?rEbnKVwRb-f^t5PP{Wef{)-9OVsBhkepB+cY%|gjYOu!m-mmB&*_d6-xHpbt+b@q&6_-*gz(h+x%-M>fd5HB-02Jc^e7W! zL;(8iLwptXFv|vTceV%-r2PFaN}l%bO`8-ieoSI>Y>@G3SVgop0q}8C7?` zNR(GW7{(njV$V<%V8lHGYf`QDc3wx9DwLW@HL5@yt_9yaOX1}fMV~s91x>&7J_BES zc1n-*8yyw>7DoT9fa8re<$_mtu4=eo1*B1X4Rnm)z49z&=)lP%JaI8$>(fgT(lj0bhM)R7C{@(`Uc;5U5=C_c`yeTt9jPW3$o zFrR{D^P0$4x7v{%{ySM--90#r0j1!UTCb#;UO^h#&fXsXwZ;aL2OK4D`EC59P>M%o zx6D!e;yE<$?bTW;RPIGI!v9zql$4A2D zC3UtoW~)nQD~rdcv#qVIwWS3jGwa`9J4AZ>icoVqAiW~Lp{%5&!^S7y&4xwLY*@9q zqRK^2!-cTE$BOUO5sHSTLnL89Xy8`LF5%Sh%24$N5xd2w@O?ZSxyCLj%TQ{dQ-pvW z2|nG9y|BTMbXt{{S?cy&_r*6m*U`nvmf(139k>u1Z8OK5ip(BDq~+=zD*hgHV4&W9 zw8-!;VEXcpS`r&4_Tq1z$idJK3Y0%9c)0AuPN;=-Frh)_peiOCXpMQ}Pe)l9*>VZ~ zBDeT(zwqw%@Wh*Sc9EHbBS`$bEt~M+BU|8IGev|Xz3){gLFVLs3}&^#ZJOSR_x>o_ z8zz=wrkPQ)^e7qdPk_+3JD~;qwQlWYX=1{V1Tfz6l3=f;9rxl@hM^Mrf{CX(KgW=w ztL9yN%j$SsuUkE4JS7ngu7&`s+-!xo+_Q%;sJ$|WFj!a%Fa;>ASJb923Ib$Fv!O~jk7ug z8ckAA{~Xc^8XBxGznI_QVy-0*ddGOvu5&D)PP*XuHzo$ddcG^$aK_bZ+jtTN0$R<( z@L5tBY!F^1_FKwnCG4hF3eXU7O5G?o?bAh&(>>)O^p~-q0<>#dAbn@z$E9>fw8fh~ z2zUS@Ga&Bf7R_35p@ASa<`C+op``nfaH4IG)UxJg+3|WhS=+xSYR#yHCeEV5T$fIz ztoE0;S0^!atm$dD2;IQa3Uhw50(Pn|OgR~6D5t!FB>LHZ?fEYtU}(a2rBO>clo(!1 zPM>$)lQhWw&9vN&w;XwYN1}&K&GzS3k(>QYJtrgAcFt%p%$jDc?z~`z6H@Cj6y;iekBGRsGi@W#=(W|tV#rn;<3#h)M^Q{`q z+uKSnJ)u0o*_~~(`qX+?Jm49=l>f%XS)dx8KUIH^7H?Z#JZbJvStk2-Yh4LT<8u#EK^hd}k!Dkg8Q&gPSLHTwO~JPb|Vl6NwUjel^CDPhnA2Ou!#7>yb1egY7# z6hD-Y&K&h*BsJ+ImBJSM5oBe)TzeJcrw&a+3i$@c<36i<$Xd<$W^Q7a#sQ95x&0r!+pmmmzi}P_aTFml&&D=oQvE!SWWR ztG}I&oYgzezxM9s^#iAG)`9R1w!lV|a*cKJzTnQuAJspV9smQC4*jNa(DrJ#1z?$@ zm&~DTj1=}5X6}-sXkw-Oj2#vDOCSv3`$M-v!}g8*!wJjalJ#dIQ-^1n;Cc?%O zxc8me(piz^>CCD%M{;ID(%#>!X(MVvn#+x8`mTr2Gy3Thcs!5h4m8iOTgiY(mX3+{nb8E%RX2FD7@<;t~Glr->}0(ozdSAGtO+m zl^02ttRAh@!=G;K@1&)mpo?CikA9oN7(G7%9AT@(>>lJ1^BhTf*Vw4^oKC154U?U& zDmuY5!2lO4)ae+3Tws#1IP)zg?%7!Mk*)^YBUDcP0YI6NH#*8Eh6<7`wMa8t-Kn;yrzThd2K^G2!oU!G(yC14v}P>HU!;93-WP4+ zE*|5K?kV-9vK52HOO3j8Ctb1X7`PAz<*&`Gs8<1Q+VUC;Kh4xgmE-5ePJH-|0W?aa zof_i>0fb?roa>X4UN=-cL{;qQHHo21`K$&R?s=Jpn!8|!<-{-2LnvYp={ijvPmGTqFMbh!Pp zy{LQ3HSIjq1}U|Tb}`0U+31bVRY*@tr@S4r&j46zVp=R4IS)pt2VF!$Nu@W5c4~Vm z^?jgo8QxXa;H}}SC5mn)c3UKmu4hr?wQya@C2F_hkM;Bb5Mkuu@+tQ=^`^a)_qI9ux#v7wezQZ_DiR6HmuyI4@Xz`h-6z-qIW*J=fIM&g*V8&6V!>BZ4xncIe%^lc=RP;dovS`$ECQ&t>!-<34hY`dI(YFlBcOaK*5b zeIJF%ek;ImcQc4Hr#Kx2)D?C;;z^*sH7+ObTc)P!toCFy$s-%9`}~H*10{RR}}JA&5F-YYx9`qK9u*@MOA;}GvIn~=V0B2_(bAR z?G!pB7q`sWnffugMOm_lmkSSntl$zptR>*bKZPybgccsdTfPYvFeStFo8$;6wFrvARZ5EO*N^=8N#lmMcH>j@%&N_A zntg{O^rFL>a$eCT8bBrDLNrX?1JX?OZ3hCfMzoTZgV_q@47X?#jd>?x0^+#KVm3pe zZ!}$gFGAB(e13}gJjC#vxVEgFs@zpG7(0q`OLUD!Ua|UZ^B}VwYp52&%FlPVIAdThp%9kL|V|40^w?_2hWKjt?%frhB z4U9DGQCt1L6E=q}8*uVfY&6yMGwWj&8;nf@BV_MR1~4oEmnh$nTj5_}Vi6FkLWtfC zt2x_QfwsQwh)p7@dcfv@SqhC>f4ea%8xYjVReX4>r9JAuyCs zoH2V|96j2-qn+NA<>fQ?#7#c7^}mBtFG9Mq`AM$*PWsXnT63cN#{`~=>q~LmQFms{ ziJPI|yK?Y~4X;FnV}U0hxT0~XgCD`3{R&U_1vsLVIv5`V3v`|8u4kMS34d!y5cy{aveFg5X^uuPFWv;)5#bR7aA+UB`A@%hE|BEqBJDSPMa{02la za>;`q5UQ0cMhM%%;Ax!7h3qJw+J)>SWVnQ{M@SM#P|AjkpjWp;@QYgYmwVku=L7QO zKWzu+I$qCkO8N&{na}zpL=p@YYKNhN8Esk=#fI70|L$bCV(rQ$Qo~j;AA`IX) zxQs1`I3j)&N`GPfd(XM!d!vXWRWucV09a=)%0s)v zTm!=M&XqdnBk@SXT=}ypR9-;yp9q&fkT|`9%?rZcm25SN=1YB2LRE2WBug3~-l=d& z5z90d=S;mZeMPkTia<2o#ijG60v`FlwihYFE*rgIms|OSFk3Xdp1`hdpSkq&0pDQQ zcxpSq4fw9ce=cqjz=5h=)LEW`pf|NnP+Bmu2J|ILJTwA@1=rtD;0cB&!a2DT{T5FS zb=TeGt!kshzJq)24Ikyo;f>CPZM?{OQ)8*~v4!81ln~8Hq}Bw#oALu(keT(6oS-T` zq=Qe@pyUhc9tr|B`d?|@ZMGFf0A&lihR0y0>?r-a1A!v*4d7icDT^I zgA(>qXYUvl8jpLD#6OZhVE7@SG*k|_hj@!I$bx-nOi1`)FPtibK%{6A{$hQtd~eXz z!Ax1EHPMsW&~nXFX0#ob<=o;A2@(p8d{At49qsf&<}F7;kb4eyK~w~dfXueEgv!`~ zJbfW)zZ6jvsSkM->0I)&UAS$t$GOJ0>@dg=LKO(OfYK?%(o7(lO#Lud2Sb0{XHQh6M-1t)EmvR^*-ov_q1AC# z$!~_m!H4+mA2sV@9PrcKVsZ5)^LLL8+Lj{p55#HRb_=dA3iE5@l<%nTEp}VFUSi@B zQLE5xly=6V8K(sYvOV5+0YpK#;GUrXDe7k`Xc|aAeC|euxab2U zo+xBSS^?m3qWcMilEXlSQ$(5R)3f|(2}b5DEHs(vAH6rl)rT&jq!9}!>?~bD4W9o+fWk&CKfIY3;uvsqOloL=Gxp= zQ{&l{Cap7a@}ZWx`MNnKy4{o7ns z!j%>QqExkUEj88A=ApKJON;r5n*rCvNBr0Zi+YpNlgnCvauV#f)pV* zJDmOXx&qYfZ)F-BUtaq9np5jUoUPgH=?r!4#9G{1tMnna0Le0zqCJR%98amCsrHIJ zbcj0UE4J?1J^1c_5j5R3fAQdN_W9s%4nV;PtY4uBY+i#q1V$`i_}6a1dsS~!LhJ4T zF`*7lDuzA_Q4d^O92Q5;s~hB0p3ymfI<)%~^QvXL`fJr5=;z;m>j}5@mHTdRIDdZM ztBMSzbTWKcnL<6Pc9*2wtWDa-4Zlv-%t-=NPk;t%6`=?gERH5~;V#Be1KqZ)0%F&D z#$Ke+s1*WRdQlU>o=2!-qM5}GH@TMpMaWXv9^1bHx-cg?&qDm|Nd^}*$`ufhKPU#dj1j3&e)0$vBJ)}zK2!q#3$;i9|8c%14 zp3Scq=I(V={99AgEF{2>{Vir&adWiw?jC8Jx7p6zB2uz!b>0+hN<5hwhj6vVaEM{A z%{^Bn4sny_WeI+L5Y7jlzkB141J%@oq>MV(FPb8#XKp@Tp%oZVppdSJbupd&Ub_nXaq~mYrPgXy|+)*Tmgkfymenw?1 zA&8UH73sw-me3ofSOTViJjMuv2ovPAz{?S2vJO1Xz#<|18;pCbp?}HEY-pr^)HwO% zA7{cpqhMjs!1(|sLjqU=CGcJ#izK(Eehg+`6^s``ejV~Fcf9z$dJUf1<{-l@ZWV==HLKdq zZWqEezna+sl*MeSR$HxW{#;tyy!gFow^;Z7bll8{Lj-@H$8Ept=*{v?{m{O|&h>qi zP=s41v@Xbyb!%rrSmE?6PsnlC0i2Nf?u*lOebwcT#P1F2L$N}#1(3T83SaPO7b5Irj*hZaSP0TJiE4Phqw zu`Yu{LHaubJbc|#GErWV?7gBVGJL)nNFCcl8lDxi=Y7n1`Ufv3OAN1|i@Ha9RV5!d zhz2w+0;hWy_ix@ibOaodE=6H4o@WNWNwXY26>_hy8~{mg`-HagZol=ZwtG8$m^Lx&Pu&-u->q8%yII ze~!RK3BP?}9Hi;WiRpe2zQ5#2n4ACbP@K1CUo`&hA`n17lfl!A8K87BcF1*EB80#2 zmY?Px@s2q0AhT$@@>ZYLHytPQ5S&JTLWD?gcblZ|AK8~UrtqKv2+6DSdd2qQr^(_i zdsypf)T~>!^v7*cCg;?6f$uc8+|@r z$zo@(bLcV@`5J8j$coWnLb!uj3kNtF$Vm`mz`d+6#n^;H^*9>45VBf&ze37-k8Qrg zV$kV@wmp+0S)Cj1n?wG~Av{D7dw-wCT53*}tgb6%z&M4@VB;|fuw0H_X)YIve|i*k z4;4ueL|mHIMa}wkrOCPi*j~ZIiH7t@w+SR_>h0Q! z9@7Ec`@LU7ju}#FLJ!3CGHJ+}t~uiBzu|Pq-Aj5i^ZYp@I~yt)H^Ev!hQ+=G0oggd zJ_-ae!kcg{Xz4Q`a~rMkMOSdqg`jC+|4p3D!s#atIsIkSl)@I3L!#n zU^2;_6%h59TKFR?v!jybGFCl^(8;)f6abaSHdn$WQ-#P>jEg8>sRTgI1{hh3;O1Cq$^w+D?c|ZgH6Gq& z2a|jN)A1RM=s7V7mQfu;VDZKv!Ho^7xO{;WBnfN{ghQHihN!9(W<0lwtY^dDMaQ+% zJ0@y5vjVGc6i9U(C>QK&;U%$T`Y3?Gb+C}rm;lYZjQVE1ct*%VDN`*yvGhegEMd=a z#@Fnkk!drDP}KIWkg(DqC{#@NjDfF>i8>Qdx*Wc?=A zB}Jvn_Hg0(pHDHdegs0$cDnx+GZoz2MF!UW6^2L=|KZ(LmI!z{;Bi_ZKkE zuF+^m-AebljGy;xX`LAO0s;Dt&fT%D)48j6R3ni&YFWeA&a|0<#@>^aQBt4oLJuXKv z&6$5?3m0Nz-4N}43J&ss_JK_l0s_PcXtTdqayqnORv-*N@Q!YP(He*GiZUzi_f_oh z7KrgRFHLHBHVe78VIe+&Y74~W;vYnJ6LLqDZoBFVw_I?Wa-gs>Z293(WhE8>4aImk zb;Do%)VN=Y#*<~FNMs$Fc?%31O68SsOk6?sDxQx0vL#Moo4ZuQ4Sy3&ar1ank$LLq zC*`coMjABJJYOrCk~TqV3^1te8JR)FN9gnwex)`l&DsY7_%e@pta!e@zai zb2aFe;AYDRupialGld)$bs+dA`@p`ED!a`VE+F`1Y$$~E3T($b6H^w`InzQ$pU(`hQ3j_C zhg*Tyyv0v~?M;H{k0IorSV7A514vA3=@zK3y5eqj$G;sbPAS*$-)*PY@CnCC{-~}iyg_6Z=}ycFf%U3|t4 z+}u+Zy0+9N6g1pk3}DXB9Z@UC+&r;~Ch}mVV#IHkSs<78)tXl_ zpJsRFRdVO`og0p=LA(!@3I#UwE$B%(!fWi_(l;UhMeN4Vn6SGY@=1Vm&maq{1#ed} z)6{z*?D}gH&aM| zX-r5is?zx3QVnwCrJW(vSFnq->06WC0cc9r9QOvvjv=XWfp_Om&$R zG=%c}WAxr7%W|mz22r?={hc!)3UIU11YcYKEwDf!na>3HdT_=cIesf@fWjaT{X{a7 zd~P(BOTn|LymC5SytjXK@gOCS3~KQ)jSxTrwZTqLzPdd6-qAD#s+1arI4}mQqOHal z_{<=yCrB}_>0{N$kq_Krpg_fL>{KzoY_(a^HbaWPTdN9mp8j2{hNWGiZf3cnEw?DA z##kLU0wMxX{>I(}amozQ?if%3JKw1_Tt{dvH;Gm2GTUR*`t0ibfF!0I#+^!|XKNOt zcDIUxw~Gj!GXpU)h@~Eg)!KBvV$J9yj+#?di>N2!Mk*^e%k=u(S6n-Xvnz4$ET~Bw z*T_W>Ew>H$HnqBlE49&y@M`fFjg{=?jz&>hn`Hzvv$S~Y>DXDqYZn!;S=lglFHuv% zU(wpt6tLM`C_*Jq>KmWv(W_Oap!sEY87JnhEpdIf zVUYwdbyvZgfu>R-aSrp!4ze+4o~%}c9V6tLvd2beTpHFjtKt^QZ*)#!MmA}~- z#7v$Mct4*07vo{t`PcON>$_^IrLc%o%_0}A=`4$#;kbnUw|ws_pE#8-^GF?!VP40% zi#`~o4AENuHD@fDHBNuzU-Aq5Vd-4wNIq%6WwcLngc+BE{PsXQSvLi;)F^9PJ$2bs zCGHxKNkws7tJZ^EX)H${DW+eCdob275aPO#tXPK&D4$86-Bd$AC+}207m6PIj$Iq2 zGFJ@`N|Z4dZ3LMT2c9R-{IzW~SmNhYJhu`z)>FT+?ufJ@{o*-cZJYZY5!{Mi8hmH~ zJh?0|OFC?+uAj2jv7Z<-*mwq(n{`$P?DsWXl|-wY3ZZ187#0 zZSVH6nJDvwpLESz#Q5eTnXZ(Ui@h)4p!g33j5z6#{?ZgqETkkKmIF>WdFWxKwwck> zuMw=qEqSnGx3hywX0F-XTon}xe_M}#Aq|332<3q2cB-WlY4IwO1TbM?!p%tU7Lowe zkaCAW)e*XY0Y;ee^-e)r`1>OJ8Rozex>;vri;?sV3i`;cZmbfW&vKyn<-=7#HSG!# zbjZA&-pEmgF&>Xx>QOuKbQ0_c+bWw8QN2qu)Z2ikN#kc2Jt$wl5isi}5|qSX`K(AP zhdlL^(?tohk?tc!v_++WUr5;v92lz2aMTzL$An}_5=*1PbI*P9HiuN>?qrhiZo14w zbRar<*nOvyJGqp<#TZhR1{yX%b~Sy&>;~!l4VVVXwmuF?wk%gMSMtsTTeh)PH{JeR z@@prHNwEyKo2(I)i-oX&yF=nQJKM)IkwPT+MYb2`9kc-fXijunnb0K^Emm5Yaia8K zYEa{O<^W7|e^qkY7A6C9o~(5;E!-Ahz9wkhv1rbTlnB#KLb{d;o~R2*e5y~`-Uh|g zN_*^OB^=!*UNt=@x@f%Dymh`3MPs*OPf?^mK07S(In_7rL^Qzp6L+Iv?whvvq4IdP z{9;iqN#A{|PwJt6{zk}GoQt?b;)!8$UDQl)1?=0Cr+YZ;V*dukHHDZ|)ikarITsWE zjpa2-gJ0BrKRGt8qyGfJIwDZy@x{MV{TCq^n<8UHoCATcJ}+BYqEa5)`#Zroirg;& zpG4VV5VeY9Pg=!cFb%Y4h&2$uz@OlVYEp(Kbi$JEhxq8gOjl=xF{aL~Fh}u1xNPi% zTNXU$h(B#kON&W3WJvXq6#Yh_fdg6?xklKBJf_UMz8!~_gL)O9uJx-}Q%4%|3@P1l z0puM84B`o>Jh!WO;LmOx+>1^+?Y3QqjQ}mV&b@As2F7FW-~T5A<>S@4far`S04LHbxh4 zubmPn?vRWJJd67+(_6|J;y8ISZuARQg%*Z#=wWU-fDS}yAGoPG&aIgD#62i~;LPxE z1*rQ?gr#mkv3USDfvUD@>hi^>_K6Yobv16Ovk#0uO=GDlNGXUs)*GW*fU;YeW}A61 z#&W0MwUM@F)s#ts!mhzZ!w#>6bxyy#{$jq2VD&j69eNgh6BOdo{WNx2fyR6jG27$^ zkrt47v~VJ&EV%;av=?-7OkqkHc%XLw_WgI&-v|xBl9u9)cPB!XhsjrrX45Yk`{0Zh z!xcHkyBSvwKd4xzC}wvaIG!|i8WQdXZzeyT4hf)!zh>_UaPHXI*IeFWVu2LOK%Rr{ z9A2oDmYOc%y|B~>X6r}AF~%l(*!LmDbaXE25oCQFFg#o=VI>;TZ+`Cqc;fnCOdHR- zN0u#skJIc{*^!!N-y_$C4Qkeu=Z}UJIHc?ZoN8`oLFm5_ByO$T79eS&1Y)~-r#+zU zUs!|t`kSI2ix}^o)Rf+Pm~#t==$mtLR7cy%8rfC8LMoNxBsE%Poj-7N=~=d;)DF zzZ5ok_6O_E9;4clP5X-1>_B^sElTL`nj@Rb)Yfxb$&aiOf6Z>k`>i&gb@sxi`sjN`zGUdHQ3dzvFuPA>rR4F@7`JiHyP#T9mrzXh`qS zzo*!B)AJP12yMQ#z*rHNg(2m(0#{-ujFEWExSzM{fb$k+MQS4`+e{f*Ux?D{@1AR_ zX$l%VFLPU1zQorl%eES=M0ZL2DDAlbxXs~~xIV-Iu!pTTBt@%a4E$@z;mZr=G=&&v zqiXw=-uwQQRK*LGC~NyP2#ex0Kz7bMGH?7A1A#`H-6JQ-wOl&2PItWvX{JSsUxxM| zST$+|#wFg`)|=6T!TC@eXpF!WT}Tr?FxlP^+n)4jiV*0u;?KGWSj+of7WcpLkSL@|_Z@OZD(I zP5wvx(lh8(F0&J&0Gz7?XqOu>6F6~9?A0C7oRq<^Q`}~|`wJX)P_*uz Ac-xW1# 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 Q>rd#Eb%7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3..8838ba97 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb..1aa94a42 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,10 +131,13 @@ 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. + if ! command -v java >/dev/null 2>&1 + then + 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 fi # Increase the maximum file descriptors if we can. @@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -198,11 +202,11 @@ fi # 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"' -# 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. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \