+ * Although MineDown provides {@link MineDown#escape(String)}, that method fails to escape events
+ * properly when using the escaped string in a replacement, so this is used instead
+ *
+ * @param string The string to escape
+ * @return The escaped string
+ */
+ @NotNull
+ private static String escapeMineDown(@NotNull String string) {
+ final StringBuilder value = new StringBuilder();
+ for (int i = 0; i < string.length(); ++i) {
+ char c = string.charAt(i);
+ boolean isEscape = c == '\\';
+ boolean isColorCode = i + 1 < string.length() && (c == 167 || c == '&');
+ boolean isEvent = c == '[' || c == ']' || c == '(' || c == ')';
+ boolean isFormatting = (c == '_' || c == '*' || c == '~' || c == '?' || c == '#') && Util.isDouble(string, i);
+ if (isEscape || isColorCode || isEvent || isFormatting) {
+ value.append('\\');
+ }
+
+ value.append(c);
+ }
+ return value.toString();
+ }
+
+ public static class Builder {
+ private String title;
+ private Version version;
+ private String description;
+ private final Map> attributions = new LinkedHashMap<>();
+ private final List buttons = new ArrayList<>();
+
+ private Builder() {
+ }
+
+ /**
+ * Set the title of the resource to display on the menu
+ *
+ * @param title The resource title
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public Builder title(@NotNull String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Set the description of the resource to display on the menu
+ *
+ * @param description The resource description
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public Builder description(@Nullable String description) {
+ this.description = description;
+ return this;
+ }
+
+ /**
+ * Set the {@link Version} of the resource to display on the menu
+ *
+ * @param version The resource version
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public Builder version(@NotNull Version version) {
+ this.version = version;
+ return this;
+ }
+
+ /**
+ * Add an attribution to the menu
+ *
+ * @param category The attribution category (e.g. {@code "Author"})
+ * @param credits {@link Credit}s to add
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public Builder credits(@NotNull String category, @NotNull Credit... credits) {
+ final List creditList = new ArrayList<>(Arrays.asList(credits));
+ attributions.putIfAbsent(category, new ArrayList<>());
+ attributions.get(category).addAll(creditList);
+ return this;
+ }
+
+ /**
+ * Add linked buttons to the menu
+ *
+ * @param links {@link Link}s to add
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public Builder buttons(@NotNull Link... links) {
+ buttons.addAll(Arrays.asList(links));
+ return this;
+ }
+
+ /**
+ * Build the {@link AboutMenu}
+ *
+ * @return The {@link Builder}
+ */
+ @NotNull
+ public AboutMenu build() {
+ if (title == null) {
+ throw new IllegalStateException("Title must be set");
+ }
+ return new AboutMenu(title, version, description, attributions, buttons);
+ }
+
+ }
+
/**
* Represents a link related to the resource
*/
@@ -274,7 +330,8 @@ public class AboutMenu {
* @param description The description of the credit (what they did)
* @return The {@link Credit}
*/
- public Credit withDescription(@Nullable String description) {
+ @NotNull
+ public Credit description(@Nullable String description) {
this.description = description;
return this;
}
@@ -285,7 +342,8 @@ public class AboutMenu {
* @param url The URL of the credit (i.e. their website)
* @return The {@link Credit}
*/
- public Credit withUrl(@Nullable String url) {
+ @NotNull
+ public Credit url(@Nullable String url) {
this.url = url;
return this;
}
@@ -296,38 +354,12 @@ public class AboutMenu {
* @param color The color of the credit
* @return The {@link Credit}
*/
- public Credit withColor(@NotNull String color) {
+ @NotNull
+ public Credit color(@NotNull String color) {
this.color = color;
return this;
}
}
- /**
- * Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
- *
- * Although MineDown provides {@link MineDown#escape(String)}, that method fails to escape events
- * properly when using the escaped string in a replacement, so this is used instead
- *
- * @param string The string to escape
- * @return The escaped string
- */
- @NotNull
- private static String escapeMineDown(@NotNull String string) {
- final StringBuilder value = new StringBuilder();
- for (int i = 0; i < string.length(); ++i) {
- char c = string.charAt(i);
- boolean isEscape = c == '\\';
- boolean isColorCode = i + 1 < string.length() && (c == 167 || c == '&');
- boolean isEvent = c == '[' || c == ']' || c == '(' || c == ')';
- boolean isFormatting = (c == '_' || c == '*' || c == '~' || c == '?' || c == '#') && Util.isDouble(string, i);
- if (isEscape || isColorCode || isEvent || isFormatting) {
- value.append('\\');
- }
-
- value.append(c);
- }
- return value.toString();
- }
-
}
diff --git a/src/main/java/net/william278/desertwell/util/ThrowingConsumer.java b/src/main/java/net/william278/desertwell/util/ThrowingConsumer.java
new file mode 100644
index 0000000..0af8ceb
--- /dev/null
+++ b/src/main/java/net/william278/desertwell/util/ThrowingConsumer.java
@@ -0,0 +1,19 @@
+package net.william278.desertwell.util;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface ThrowingConsumer extends Consumer {
+
+ @Override
+ default void accept(final T elem) {
+ try {
+ acceptThrows(elem);
+ } catch (final Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ void acceptThrows(T elem) throws Exception;
+
+}
\ No newline at end of file
diff --git a/src/main/java/net/william278/desertwell/util/UpdateChecker.java b/src/main/java/net/william278/desertwell/util/UpdateChecker.java
new file mode 100644
index 0000000..afb787d
--- /dev/null
+++ b/src/main/java/net/william278/desertwell/util/UpdateChecker.java
@@ -0,0 +1,181 @@
+package net.william278.desertwell.util;
+
+import org.jetbrains.annotations.NotNull;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.regex.Pattern;
+
+/**
+ * Utility for comparing a {@link Version} against the latest version on various {@link Endpoint}s
+ */
+@SuppressWarnings("unused")
+public class UpdateChecker {
+ private final Endpoint endpoint;
+ private final Version currentVersion;
+ private final String versionMetaDelimiter;
+ private final String resource;
+
+ private UpdateChecker(@NotNull Endpoint endpoint, @NotNull Version currentVersion,
+ @NotNull String versionMetaDelimiter, @NotNull String resource) {
+ this.endpoint = endpoint;
+ this.currentVersion = currentVersion;
+ this.versionMetaDelimiter = versionMetaDelimiter;
+ this.resource = resource;
+ }
+
+ @NotNull
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Query SpigotMC for the latest {@link Version} of the plugin
+ *
+ * @return A {@link CompletableFuture} containing the latest {@link Version} of the plugin
+ */
+ public CompletableFuture check() {
+ return CompletableFuture
+ .supplyAsync(() -> new Completed(this, Version.fromString(endpoint.query(resource), versionMetaDelimiter)))
+ .exceptionally(throwable -> new Completed(this, currentVersion));
+ }
+
+ public static class Builder {
+ private Endpoint endpoint = Endpoint.SPIGOT;
+ private Version currentVersion;
+ private String versionMetaDelimiter = Version.META_DELIMITER;
+ private String resource;
+
+ private Builder() {
+ }
+
+ @NotNull
+ public Builder endpoint(@NotNull Endpoint endpoint) {
+ this.endpoint = endpoint;
+ return this;
+ }
+
+ @NotNull
+ public Builder currentVersion(@NotNull Version currentVersion) {
+ this.currentVersion = currentVersion;
+ return this;
+ }
+
+ @NotNull
+ public Builder versionMetaDelimiter(@NotNull String versionMetaDelimiter) {
+ this.versionMetaDelimiter = versionMetaDelimiter;
+ return this;
+ }
+
+ @NotNull
+ public Builder resource(@NotNull String resource) {
+ this.resource = resource;
+ return this;
+ }
+
+ @NotNull
+ public UpdateChecker build() {
+ if (currentVersion == null) {
+ throw new IllegalStateException("Current version is not set");
+ }
+ if (resource == null) {
+ throw new IllegalStateException("Resource is not set");
+ }
+ return new UpdateChecker(endpoint, currentVersion, versionMetaDelimiter, resource);
+ }
+
+ }
+
+ /**
+ * Represents endpoints from which the latest version can be queried
+ */
+ public enum Endpoint {
+ SPIGOT((resource -> {
+ final String url = formatId("https://api.spigotmc.org/legacy/update.php?resource={id}", resource);
+ try (final InputStreamReader reader = new InputStreamReader(new URL(url).openConnection().getInputStream())) {
+ return new BufferedReader(reader).readLine();
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to fetch latest version", e);
+ }
+ })),
+ POLYMART((resource -> {
+ final String url = formatId("https://api.polymart.org/v1/getResourceInfoSimple/?resource_id={id}&key=version", resource);
+ try (final InputStreamReader reader = new InputStreamReader(new URL(url).openConnection().getInputStream())) {
+ return new BufferedReader(reader).readLine();
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to fetch latest version", e);
+ }
+ })),
+ MODRINTH((resource -> {
+ final String url = formatId("https://api.modrinth.com/v2/project/{id}/version", resource);
+ try (final InputStreamReader reader = new InputStreamReader(new URL(url).openConnection().getInputStream())) {
+ final JSONArray array = new JSONArray(new BufferedReader(reader).readLine());
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject object = array.getJSONObject(i);
+ if (object.getString("version_type").equals("release")) {
+ return object.getString("version_number");
+ }
+ }
+ throw new IllegalStateException("No versions found");
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to fetch latest version", e);
+ }
+ })),
+ GITHUB((resource -> {
+ final String url = formatId("https://api.github.com/repos/{id}/releases/latest", resource);
+ try (final InputStreamReader reader = new InputStreamReader(new URL(url).openConnection().getInputStream())) {
+ return new JSONObject(new BufferedReader(reader).readLine()).getString("tag_name");
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to fetch latest version", e);
+ }
+ }));
+
+ private final Function queryFunction;
+
+ Endpoint(@NotNull Function queryFunction) {
+ this.queryFunction = queryFunction;
+ }
+
+ @NotNull
+ public String query(@NotNull String resource) {
+ return queryFunction.apply(resource);
+ }
+
+ @NotNull
+ private static String formatId(@NotNull String endpoint, @NotNull String resource) {
+ return endpoint.replaceAll(Pattern.quote("{id}"), resource);
+ }
+ }
+
+ public static class Completed {
+ private final UpdateChecker checker;
+ private final Version latestVersion;
+
+ private Completed(@NotNull UpdateChecker checker, @NotNull Version latestVersion) {
+ this.checker = checker;
+ this.latestVersion = latestVersion;
+ }
+
+ @NotNull
+ public Version getLatestVersion() {
+ return latestVersion;
+ }
+
+ @NotNull
+ public Version getCurrentVersion() {
+ return checker.currentVersion;
+ }
+
+ public boolean isUpToDate() {
+ return checker.currentVersion.compareTo(latestVersion) >= 0;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/net/william278/desertwell/Version.java b/src/main/java/net/william278/desertwell/util/Version.java
similarity index 85%
rename from src/main/java/net/william278/desertwell/Version.java
rename to src/main/java/net/william278/desertwell/util/Version.java
index d607b1c..e8d4d9b 100644
--- a/src/main/java/net/william278/desertwell/Version.java
+++ b/src/main/java/net/william278/desertwell/util/Version.java
@@ -1,4 +1,4 @@
-package net.william278.desertwell;
+package net.william278.desertwell.util;
import org.jetbrains.annotations.NotNull;
@@ -11,9 +11,9 @@ import java.util.regex.Pattern;
*/
@SuppressWarnings("unused")
public class Version implements Comparable {
- private final String VERSION_DELIMITER = ".";
- private static final String MINECRAFT_META_DELIMITER = "-";
- private static final String PLUGIN_META_DELIMITER = "+";
+ public final String VERSION_DELIMITER = ".";
+ public static final String META_DELIMITER = "-";
+
// Major, minor and patch version numbers
private int[] versions = new int[]{};
@NotNull
@@ -46,23 +46,11 @@ public class Version implements Comparable {
*
* @param versionString The version string to parse
* @return The {@link Version}
- * @implNote The default meta delimiter that will be used is {@link #PLUGIN_META_DELIMITER}
+ * @implNote The default meta delimiter that will be used is {@link #META_DELIMITER}
*/
@NotNull
public static Version fromString(@NotNull String versionString) {
- return new Version(versionString, PLUGIN_META_DELIMITER);
- }
-
- /**
- * Create a new {@link Version} by parsing a Minecraft string
- *
- * @param versionString The Minecraft version string to parse
- * @return The {@link Version}
- * @implNote The meta delimiter that will be used is {@link #MINECRAFT_META_DELIMITER}
- */
- @NotNull
- public static Version fromMinecraftVersionString(@NotNull String versionString) {
- return new Version(versionString, MINECRAFT_META_DELIMITER);
+ return new Version(versionString, META_DELIMITER);
}
/**
diff --git a/src/test/java/net/william278/desertwell/UpdateCheckerTests.java b/src/test/java/net/william278/desertwell/UpdateCheckerTests.java
index 5efa054..a0547c3 100644
--- a/src/test/java/net/william278/desertwell/UpdateCheckerTests.java
+++ b/src/test/java/net/william278/desertwell/UpdateCheckerTests.java
@@ -1,15 +1,53 @@
package net.william278.desertwell;
+import net.william278.desertwell.util.UpdateChecker;
+import net.william278.desertwell.util.Version;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class UpdateCheckerTests {
@Test
- public void testUpdateChecker() {
- // Tests against the HuskSync resource ID
- final UpdateChecker updateChecker = UpdateChecker.create(Version.fromString("1.0.0"), 97144);
- Assertions.assertFalse(updateChecker.isUpToDate().join());
+ public void testSpigotEndpoint() {
+ final UpdateChecker updateChecker = UpdateChecker.builder()
+ .currentVersion(Version.fromString("1.0.0"))
+ .resource("97144")
+ .build();
+
+ Assertions.assertFalse(updateChecker.check().join().isUpToDate());
+ }
+
+ @Test
+ public void testGitHubEndpoint() {
+ final UpdateChecker updateChecker = UpdateChecker.builder()
+ .currentVersion(Version.fromString("1.0.0"))
+ .endpoint(UpdateChecker.Endpoint.GITHUB)
+ .resource("WiIIiam278/HuskHomes2")
+ .build();
+
+ Assertions.assertFalse(updateChecker.check().join().isUpToDate());
+ }
+
+ @Test
+ public void testModrinthEndpoint() {
+ final UpdateChecker updateChecker = UpdateChecker.builder()
+ .currentVersion(Version.fromString("1.0.0"))
+ .endpoint(UpdateChecker.Endpoint.MODRINTH)
+ .resource("huskhomes")
+ .build();
+
+ Assertions.assertFalse(updateChecker.check().join().isUpToDate());
+ }
+
+ @Test
+ public void testPolymartEndpoint() {
+ final UpdateChecker updateChecker = UpdateChecker.builder()
+ .currentVersion(Version.fromString("1.0.0"))
+ .endpoint(UpdateChecker.Endpoint.POLYMART)
+ .resource("284")
+ .build();
+
+ Assertions.assertFalse(updateChecker.check().join().isUpToDate());
}
}
diff --git a/src/test/java/net/william278/desertwell/VersionTests.java b/src/test/java/net/william278/desertwell/VersionTests.java
index 0324544..45b8e19 100644
--- a/src/test/java/net/william278/desertwell/VersionTests.java
+++ b/src/test/java/net/william278/desertwell/VersionTests.java
@@ -1,5 +1,6 @@
package net.william278.desertwell;
+import net.william278.desertwell.util.Version;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -23,15 +24,15 @@ public class VersionTests {
}
@Test
- public void testVersionComparingWithMetadata() {
- final Version oldVersion = Version.fromString("1.0.0+dev");
- final Version newVersion = Version.fromString("1.0.1+snapshot-123");
+ public void testVersionComparingWithMetaDelimiter() {
+ final Version oldVersion = Version.fromString("1.0.0+dev", "+");
+ final Version newVersion = Version.fromString("1.0.1+snapshot-123", "+");
Assertions.assertTrue(oldVersion.compareTo(newVersion) < 0);
}
@Test
public void testParsingMinecraftVersion() {
- final Version version = Version.fromMinecraftVersionString("1.2.3-SNAPSHOT");
+ final Version version = Version.fromString("1.2.3-SNAPSHOT");
Assertions.assertEquals(1, version.getMajor());
Assertions.assertEquals(2, version.getMinor());
Assertions.assertEquals(3, version.getPatch());