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/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..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,129 +30,27 @@ 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 { @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; + public record RegisterGroupRequest( + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String website + ) { - @JsonCreator - public RegisterGroupRequest( - final String name, - final String groupCoordinates, - final String website - ) { - this.name = name; - this.groupCoordinates = groupCoordinates; - this.website = 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); - } + @JsonCreator + public RegisterGroupRequest { } - @Override - public int hashCode() { - return Objects.hash(this.name, this.groupCoordinates, this.website); } - @Override - public String toString() { - return "RegisterGroupRequest[" + - "name=" + this.name + ", " + - "groupCoordinates=" + this.groupCoordinates + ", " + - "website=" + this.website + ']'; - } - } - 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); - } - - @Override - public int hashCode() { - return Objects.hash(this.group); - } + record GroupRegistered(Group group) implements Response { - @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..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 @@ -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,35 +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/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..573fde7f --- /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/build.sbt b/build.sbt index a0b5be30..1d09aae3 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" @@ -114,34 +114,43 @@ lazy val junit = "org.junit.jupiter" % "junit-jupiter-api" % "5.7.2" % Test lazy val jupiterInterface = "net.aichler" % "jupiter-interface" % "0.9.1" % Test -// Play jackson uses 2.11, but 2.12 is backwards compatible -lazy val jacksonDataBind = "com.fasterxml.jackson.core" % "jackson-databind" % "2.12.5" -lazy val jacksonDataTypeJsr310 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.12.5" -lazy val jacksonDataformatXml = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.12.5" -lazy val jacksonDataformatCbor = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % "2.12.5" -lazy val jacksonDatatypeJdk8 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.12.5" -lazy val jacksonParameterNames = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.12.5" -lazy val jacksonParanamer = "com.fasterxml.jackson.module" % "jackson-module-paranamer" % "2.12.5" -lazy val jacksonScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.12.5" -lazy val jacksonGuava = "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % "2.12.5" -lazy val jacksonPcollections = "com.fasterxml.jackson.datatype" % "jackson-datatype-pcollections" % "2.12.5" +// Play jackson uses 2.11, but 2.13 is backwards compatible +lazy val jacksonDataBind = "com.fasterxml.jackson.core" % "jackson-databind" % "2.13.3" +lazy val jacksonDataTypeJsr310 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jsr310" % "2.13.3" +lazy val jacksonDataformatXml = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "2.13.3" +lazy val jacksonDataformatCbor = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-cbor" % "2.13.3" +lazy val jacksonDatatypeJdk8 = "com.fasterxml.jackson.datatype" % "jackson-datatype-jdk8" % "2.13.3" +lazy val jacksonParameterNames = "com.fasterxml.jackson.module" % "jackson-module-parameter-names" % "2.13.3" +lazy val jacksonParanamer = "com.fasterxml.jackson.module" % "jackson-module-paranamer" % "2.13.3" +lazy val jacksonScala = "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.13.3" +lazy val jacksonGuava = "com.fasterxml.jackson.datatype" % "jackson-datatype-guava" % "2.13.3" +lazy val jacksonPcollections = "com.fasterxml.jackson.datatype" % "jackson-datatype-pcollections" % "2.13.3" // 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.4.Final" -lazy val postgres = "org.postgresql" % "postgresql" % "42.3.5" -lazy val hibernateTypes = "com.vladmihalcea" % "hibernate-types-55" % "2.14.0" +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.18.0" -lazy val guice = "com.google.inject" % "guice" % "5.0.1" +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/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java similarity index 70% rename from artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java rename to downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java index 8a71aa45..a3b29fe8 100644 --- a/artifact-impl/src/main/java/org/spongepowered/downloads/artifact/errors/GitRemoteValidationException.java +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/ArtifactCollection.java @@ -22,19 +22,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifact.errors; +package org.spongepowered.downloads.api; -import com.lightbend.lagom.javadsl.api.transport.TransportErrorCode; -import com.lightbend.lagom.javadsl.api.transport.TransportException; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import io.vavr.collection.List; -public class GitRemoteValidationException extends TransportException { +@JsonDeserialize +public final record ArtifactCollection( + @JsonProperty("assets") List components, + @JsonProperty("coordinates") MavenCoordinates coordinates +) { - public GitRemoteValidationException(String message) { - super(TransportErrorCode.BadRequest, message); - } - - public GitRemoteValidationException(String message, final Throwable cause) { - super(TransportErrorCode.BadRequest, message, cause); + @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/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java b/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java similarity index 78% rename from artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java rename to downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java index 03612b57..4efe1286 100644 --- a/artifact-api/src/main/java/org/spongepowered/downloads/artifact/api/Ordering.java +++ b/downloads-api/src/main/java/org/spongepowered/downloads/api/Group.java @@ -22,21 +22,21 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -package org.spongepowered.downloads.artifact.api; +package org.spongepowered.downloads.api; -import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @JsonDeserialize -public enum Ordering { - Ascending("asc"), - Descending("desc"); +public record Group( + @JsonProperty(required = true) String groupCoordinates, + @JsonProperty(required = true) String name, + @JsonProperty(required = true) String website +) { - @JsonValue - public final String representation; - - - Ordering(final String representation) { - this.representation = representation; + @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/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/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/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/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()); + } } 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()