Skip to content

Commit a8dc337

Browse files
vLuckyyyRollczi
andauthored
GH-48 Introduce simple update checker, using Modrinth api. (#48)
* Introduce simple update checker, using Modrinth api. * Use old eternalcombat version for testing purposes. * Use `org.json` for json parsing. * Fix. * Fix. * Cleanup and use gson --------- Co-authored-by: Rollczi <[email protected]>
1 parent 005ebec commit a8dc337

File tree

10 files changed

+268
-0
lines changed

10 files changed

+268
-0
lines changed

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
}
44

55
repositories {
6+
mavenCentral()
67
gradlePluginPortal()
78
}
89

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
plugins {
2+
`commons-java-17`
3+
`commons-repositories`
4+
}
5+
6+
dependencies {
7+
implementation(project(":eternalcode-commons-updater"))
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.eternalcode.commons.updater.example;
2+
3+
import com.eternalcode.commons.updater.UpdateResult;
4+
5+
public class ExampleChecker {
6+
7+
private static final String OLD_ETERNALCOMBAT_VERSION = "1.3.3";
8+
9+
public static void main(String[] args) {
10+
ExampleUpdateService updateService = new ExampleUpdateService();
11+
12+
UpdateResult modrinthResult = updateService.checkModrinth("EternalCombat", OLD_ETERNALCOMBAT_VERSION);
13+
System.out.println("Modrinth update available: " + modrinthResult.isUpdateAvailable());
14+
if (modrinthResult.isUpdateAvailable()) {
15+
System.out.println("Latest: " + modrinthResult.latestVersion());
16+
System.out.println("Download: " + modrinthResult.downloadUrl());
17+
System.out.println("Release page: " + modrinthResult.releaseUrl());
18+
}
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.eternalcode.commons.updater.example;
2+
3+
import com.eternalcode.commons.updater.UpdateResult;
4+
import com.eternalcode.commons.updater.Version;
5+
import com.eternalcode.commons.updater.impl.ModrinthUpdateChecker;
6+
7+
public final class ExampleUpdateService {
8+
private final ModrinthUpdateChecker modrinthChecker = new ModrinthUpdateChecker();
9+
10+
public UpdateResult checkModrinth(String projectId, String currentVersion) {
11+
return modrinthChecker.check(projectId, new Version(currentVersion));
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
`commons-java-17`
3+
`commons-publish`
4+
`commons-repositories`
5+
`commons-java-unit-test`
6+
}
7+
8+
tasks.test {
9+
useJUnitPlatform()
10+
}
11+
12+
13+
dependencies {
14+
api("com.google.code.gson:gson:2.13.1")
15+
api(project(":eternalcode-commons-shared"))
16+
17+
api("org.jetbrains:annotations:24.1.0")
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.eternalcode.commons.updater;
2+
3+
public interface UpdateChecker {
4+
5+
UpdateResult check(String projectId, Version currentVersion);
6+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.eternalcode.commons.updater;
2+
3+
public record UpdateResult(Version currentVersion, Version latestVersion, String downloadUrl, String releaseUrl) {
4+
5+
public static UpdateResult empty(Version currentVersion) {
6+
return new UpdateResult(currentVersion, currentVersion, null, null);
7+
}
8+
9+
public boolean isUpdateAvailable() {
10+
return this.latestVersion.isNewerThan(this.currentVersion);
11+
}
12+
13+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.eternalcode.commons.updater;
2+
3+
import java.util.Arrays;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
public class Version implements Comparable<Version> {
7+
8+
private static final int DEFAULT_VERSION_COMPONENT_VALUE = 0;
9+
10+
private final String value;
11+
private final int[] versionComponents;
12+
13+
public Version(String version) {
14+
if (version == null || version.trim().isEmpty()) {
15+
throw new IllegalArgumentException("Version cannot be null or empty");
16+
}
17+
18+
this.value = version.trim();
19+
this.versionComponents = parseVersion(this.value);
20+
}
21+
22+
private int[] parseVersion(String version) {
23+
String cleaned = cleanVersion(version);
24+
String[] rawVersionComponents = cleaned.split("\\.");
25+
int[] versionComponents = new int[rawVersionComponents.length];
26+
27+
for (int i = 0; i < rawVersionComponents.length; i++) {
28+
try {
29+
versionComponents[i] = Integer.parseInt(rawVersionComponents[i]);
30+
}
31+
catch (NumberFormatException exception) {
32+
throw new IllegalArgumentException("Invalid version format: " + version);
33+
}
34+
}
35+
36+
return versionComponents;
37+
}
38+
39+
private static String cleanVersion(String version) {
40+
String cleaned = version.startsWith("v") ? version.substring(1) : version;
41+
int dashIndex = cleaned.indexOf('-');
42+
if (dashIndex > 0) {
43+
return cleaned.substring(0, dashIndex);
44+
}
45+
46+
return cleaned;
47+
}
48+
49+
@Override
50+
public int compareTo(@NotNull Version other) {
51+
int maxLength = Math.max(this.versionComponents.length, other.versionComponents.length);
52+
53+
for (int i = 0; i < maxLength; i++) {
54+
int thisComponent = getComponentAtIndex(i, this);
55+
int otherComponent = getComponentAtIndex(i, other);
56+
57+
int result = Integer.compare(thisComponent, otherComponent);
58+
if (result != 0) {
59+
return result;
60+
}
61+
}
62+
63+
return 0;
64+
}
65+
66+
private int getComponentAtIndex(int index, Version version) {
67+
return index < version.versionComponents.length
68+
? version.versionComponents[index]
69+
: DEFAULT_VERSION_COMPONENT_VALUE;
70+
}
71+
72+
public boolean isNewerThan(Version other) {
73+
return this.compareTo(other) > 0;
74+
}
75+
76+
@Override
77+
public boolean equals(Object obj) {
78+
if (this == obj) {
79+
return true;
80+
}
81+
82+
if (obj == null || getClass() != obj.getClass()) {
83+
return false;
84+
}
85+
86+
Version version = (Version) obj;
87+
return this.compareTo(version) == 0;
88+
}
89+
90+
@Override
91+
public int hashCode() {
92+
return Arrays.hashCode(versionComponents);
93+
}
94+
95+
@Override
96+
public String toString() {
97+
return value;
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.eternalcode.commons.updater.impl;
2+
3+
import com.eternalcode.commons.updater.UpdateChecker;
4+
import com.eternalcode.commons.updater.UpdateResult;
5+
import com.eternalcode.commons.updater.Version;
6+
import com.google.gson.Gson;
7+
import com.google.gson.JsonParseException;
8+
import com.google.gson.annotations.SerializedName;
9+
import com.google.gson.reflect.TypeToken;
10+
import java.net.URI;
11+
import java.net.http.HttpClient;
12+
import java.net.http.HttpRequest;
13+
import java.net.http.HttpResponse;
14+
import java.time.Duration;
15+
import java.util.List;
16+
import java.util.Objects;
17+
18+
public final class ModrinthUpdateChecker implements UpdateChecker {
19+
20+
private static final String API_BASE_URL = "https://api.modrinth.com/v2";
21+
private static final String MODRINTH_BASE_URL = "https://modrinth.com/plugin";
22+
private static final String USER_AGENT = "UpdateChecker/1.0";
23+
24+
private static final Gson GSON = new Gson();
25+
26+
private final HttpClient client = HttpClient.newBuilder()
27+
.connectTimeout(Duration.ofSeconds(60))
28+
.build();
29+
30+
@Override
31+
public UpdateResult check(String projectId, Version currentVersion) {
32+
if (projectId == null || projectId.isBlank()) {
33+
throw new IllegalArgumentException("Project ID cannot be null or empty");
34+
}
35+
36+
try {
37+
HttpRequest request = HttpRequest.newBuilder()
38+
.uri(URI.create(API_BASE_URL + "/project/" + projectId + "/version"))
39+
.header("User-Agent", USER_AGENT)
40+
.timeout(Duration.ofSeconds(30))
41+
.build();
42+
43+
HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());
44+
if (response.statusCode() != 200) {
45+
return UpdateResult.empty(currentVersion);
46+
}
47+
48+
return this.parseVersionResponse(response.body(), currentVersion, projectId);
49+
}
50+
catch (Exception exception) {
51+
throw new RuntimeException("Failed to check Modrinth updates for project: " + projectId, exception);
52+
}
53+
}
54+
55+
private UpdateResult parseVersionResponse(String json, Version currentVersion, String projectId) {
56+
try {
57+
List<ModrinthVersion> versions = GSON.fromJson(json, new TypeToken<>(){});
58+
if (versions == null || versions.isEmpty()) {
59+
return UpdateResult.empty(currentVersion);
60+
}
61+
62+
ModrinthVersion latestVersionData = versions.get(0);
63+
String versionNumber = latestVersionData.versionNumber();
64+
if (versionNumber == null || versionNumber.trim().isEmpty()) {
65+
return UpdateResult.empty(currentVersion);
66+
}
67+
68+
String releaseUrl = MODRINTH_BASE_URL + "/" + projectId + "/version/" + versionNumber;
69+
String downloadUrl = latestVersionData.files().stream()
70+
.map(modrinthFile -> modrinthFile.url())
71+
.filter(obj -> Objects.nonNull(obj))
72+
.findFirst()
73+
.orElse(releaseUrl);
74+
75+
Version latestVersion = new Version(versionNumber);
76+
return new UpdateResult(currentVersion, latestVersion, downloadUrl, releaseUrl);
77+
}
78+
catch (JsonParseException exception) {
79+
return UpdateResult.empty(currentVersion);
80+
}
81+
}
82+
83+
private record ModrinthVersion(@SerializedName("version_number") String versionNumber, List<ModrinthFile> files) {
84+
}
85+
86+
private record ModrinthFile(String url) {
87+
}
88+
}

settings.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ include(":eternalcode-commons-bukkit")
44
include(":eternalcode-commons-adventure")
55
include(":eternalcode-commons-shared")
66
include("eternalcode-commons-folia")
7+
include("eternalcode-commons-updater")
8+
include("eternalcode-commons-updater-example")

0 commit comments

Comments
 (0)