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()