diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 8cd15cd..a116a08 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -3,6 +3,7 @@ plugins { } repositories { + mavenCentral() gradlePluginPortal() } diff --git a/eternalcode-commons-updater-example/build.gradle.kts b/eternalcode-commons-updater-example/build.gradle.kts new file mode 100644 index 0000000..d4ebdee --- /dev/null +++ b/eternalcode-commons-updater-example/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `commons-java-17` + `commons-repositories` +} + +dependencies { + implementation(project(":eternalcode-commons-updater")) +} diff --git a/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleChecker.java b/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleChecker.java new file mode 100644 index 0000000..feef245 --- /dev/null +++ b/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleChecker.java @@ -0,0 +1,20 @@ +package com.eternalcode.commons.updater.example; + +import com.eternalcode.commons.updater.UpdateResult; + +public class ExampleChecker { + + private static final String OLD_ETERNALCOMBAT_VERSION = "1.3.3"; + + public static void main(String[] args) { + ExampleUpdateService updateService = new ExampleUpdateService(); + + UpdateResult modrinthResult = updateService.checkModrinth("EternalCombat", OLD_ETERNALCOMBAT_VERSION); + System.out.println("Modrinth update available: " + modrinthResult.isUpdateAvailable()); + if (modrinthResult.isUpdateAvailable()) { + System.out.println("Latest: " + modrinthResult.latestVersion()); + System.out.println("Download: " + modrinthResult.downloadUrl()); + System.out.println("Release page: " + modrinthResult.releaseUrl()); + } + } +} diff --git a/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleUpdateService.java b/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleUpdateService.java new file mode 100644 index 0000000..893063f --- /dev/null +++ b/eternalcode-commons-updater-example/src/main/java/com/eternalcode/commons/updater/example/ExampleUpdateService.java @@ -0,0 +1,13 @@ +package com.eternalcode.commons.updater.example; + +import com.eternalcode.commons.updater.UpdateResult; +import com.eternalcode.commons.updater.Version; +import com.eternalcode.commons.updater.impl.ModrinthUpdateChecker; + +public final class ExampleUpdateService { + private final ModrinthUpdateChecker modrinthChecker = new ModrinthUpdateChecker(); + + public UpdateResult checkModrinth(String projectId, String currentVersion) { + return modrinthChecker.check(projectId, new Version(currentVersion)); + } +} diff --git a/eternalcode-commons-updater/build.gradle.kts b/eternalcode-commons-updater/build.gradle.kts new file mode 100644 index 0000000..bc67ab6 --- /dev/null +++ b/eternalcode-commons-updater/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + `commons-java-17` + `commons-publish` + `commons-repositories` + `commons-java-unit-test` +} + +tasks.test { + useJUnitPlatform() +} + + +dependencies { + api("com.google.code.gson:gson:2.13.1") + api(project(":eternalcode-commons-shared")) + + api("org.jetbrains:annotations:24.1.0") +} diff --git a/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateChecker.java b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateChecker.java new file mode 100644 index 0000000..b599f91 --- /dev/null +++ b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateChecker.java @@ -0,0 +1,6 @@ +package com.eternalcode.commons.updater; + +public interface UpdateChecker { + + UpdateResult check(String projectId, Version currentVersion); +} diff --git a/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateResult.java b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateResult.java new file mode 100644 index 0000000..ea8d4e1 --- /dev/null +++ b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/UpdateResult.java @@ -0,0 +1,13 @@ +package com.eternalcode.commons.updater; + +public record UpdateResult(Version currentVersion, Version latestVersion, String downloadUrl, String releaseUrl) { + + public static UpdateResult empty(Version currentVersion) { + return new UpdateResult(currentVersion, currentVersion, null, null); + } + + public boolean isUpdateAvailable() { + return this.latestVersion.isNewerThan(this.currentVersion); + } + +} diff --git a/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/Version.java b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/Version.java new file mode 100644 index 0000000..0215db7 --- /dev/null +++ b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/Version.java @@ -0,0 +1,99 @@ +package com.eternalcode.commons.updater; + +import java.util.Arrays; +import org.jetbrains.annotations.NotNull; + +public class Version implements Comparable { + + private static final int DEFAULT_VERSION_COMPONENT_VALUE = 0; + + private final String value; + private final int[] versionComponents; + + public Version(String version) { + if (version == null || version.trim().isEmpty()) { + throw new IllegalArgumentException("Version cannot be null or empty"); + } + + this.value = version.trim(); + this.versionComponents = parseVersion(this.value); + } + + private int[] parseVersion(String version) { + String cleaned = cleanVersion(version); + String[] rawVersionComponents = cleaned.split("\\."); + int[] versionComponents = new int[rawVersionComponents.length]; + + for (int i = 0; i < rawVersionComponents.length; i++) { + try { + versionComponents[i] = Integer.parseInt(rawVersionComponents[i]); + } + catch (NumberFormatException exception) { + throw new IllegalArgumentException("Invalid version format: " + version); + } + } + + return versionComponents; + } + + private static String cleanVersion(String version) { + String cleaned = version.startsWith("v") ? version.substring(1) : version; + int dashIndex = cleaned.indexOf('-'); + if (dashIndex > 0) { + return cleaned.substring(0, dashIndex); + } + + return cleaned; + } + + @Override + public int compareTo(@NotNull Version other) { + int maxLength = Math.max(this.versionComponents.length, other.versionComponents.length); + + for (int i = 0; i < maxLength; i++) { + int thisComponent = getComponentAtIndex(i, this); + int otherComponent = getComponentAtIndex(i, other); + + int result = Integer.compare(thisComponent, otherComponent); + if (result != 0) { + return result; + } + } + + return 0; + } + + private int getComponentAtIndex(int index, Version version) { + return index < version.versionComponents.length + ? version.versionComponents[index] + : DEFAULT_VERSION_COMPONENT_VALUE; + } + + public boolean isNewerThan(Version other) { + return this.compareTo(other) > 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + Version version = (Version) obj; + return this.compareTo(version) == 0; + } + + @Override + public int hashCode() { + return Arrays.hashCode(versionComponents); + } + + @Override + public String toString() { + return value; + } +} diff --git a/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/impl/ModrinthUpdateChecker.java b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/impl/ModrinthUpdateChecker.java new file mode 100644 index 0000000..f875259 --- /dev/null +++ b/eternalcode-commons-updater/src/main/java/com/eternalcode/commons/updater/impl/ModrinthUpdateChecker.java @@ -0,0 +1,88 @@ +package com.eternalcode.commons.updater.impl; + +import com.eternalcode.commons.updater.UpdateChecker; +import com.eternalcode.commons.updater.UpdateResult; +import com.eternalcode.commons.updater.Version; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.List; +import java.util.Objects; + +public final class ModrinthUpdateChecker implements UpdateChecker { + + private static final String API_BASE_URL = "https://api.modrinth.com/v2"; + private static final String MODRINTH_BASE_URL = "https://modrinth.com/plugin"; + private static final String USER_AGENT = "UpdateChecker/1.0"; + + private static final Gson GSON = new Gson(); + + private final HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(60)) + .build(); + + @Override + public UpdateResult check(String projectId, Version currentVersion) { + if (projectId == null || projectId.isBlank()) { + throw new IllegalArgumentException("Project ID cannot be null or empty"); + } + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(API_BASE_URL + "/project/" + projectId + "/version")) + .header("User-Agent", USER_AGENT) + .timeout(Duration.ofSeconds(30)) + .build(); + + HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + return UpdateResult.empty(currentVersion); + } + + return this.parseVersionResponse(response.body(), currentVersion, projectId); + } + catch (Exception exception) { + throw new RuntimeException("Failed to check Modrinth updates for project: " + projectId, exception); + } + } + + private UpdateResult parseVersionResponse(String json, Version currentVersion, String projectId) { + try { + List versions = GSON.fromJson(json, new TypeToken<>(){}); + if (versions == null || versions.isEmpty()) { + return UpdateResult.empty(currentVersion); + } + + ModrinthVersion latestVersionData = versions.get(0); + String versionNumber = latestVersionData.versionNumber(); + if (versionNumber == null || versionNumber.trim().isEmpty()) { + return UpdateResult.empty(currentVersion); + } + + String releaseUrl = MODRINTH_BASE_URL + "/" + projectId + "/version/" + versionNumber; + String downloadUrl = latestVersionData.files().stream() + .map(modrinthFile -> modrinthFile.url()) + .filter(obj -> Objects.nonNull(obj)) + .findFirst() + .orElse(releaseUrl); + + Version latestVersion = new Version(versionNumber); + return new UpdateResult(currentVersion, latestVersion, downloadUrl, releaseUrl); + } + catch (JsonParseException exception) { + return UpdateResult.empty(currentVersion); + } + } + + private record ModrinthVersion(@SerializedName("version_number") String versionNumber, List files) { + } + + private record ModrinthFile(String url) { + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 661c551..8aa583e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,3 +4,5 @@ include(":eternalcode-commons-bukkit") include(":eternalcode-commons-adventure") include(":eternalcode-commons-shared") include("eternalcode-commons-folia") +include("eternalcode-commons-updater") +include("eternalcode-commons-updater-example")