Use builders for constructing, refactor, add new version check endpoints

dependabot/gradle/net.kyori-adventure-api-4.14.0
William 2 years ago
parent c7316c0767
commit 067860e159
No known key found for this signature in database

@ -13,6 +13,9 @@ DesertWell is a simple library providing various utilities to aid Minecraft plug
To create an about menu, use `AboutMenu#create(title)` with the resource name, then use the various builder methods to
build out the menu.
<details>
<summary>Code snippet</summary>
```java
public class ExamplePlugin extends JavaPlugin {
@ -41,12 +44,16 @@ public class ExamplePlugin extends JavaPlugin {
}
```
</details>
### Version
`Version.class` provides a simple way to compare semantic plugin and Minecraft versions. `VersionChecker.class` provides
a utility for querying Spigot resources for the latest version of a plugin and comparing with the current version in
order to check for updates.
<details>
<summary>Code snippet</summary>
```java
public class ExamplePlugin extends JavaPlugin {
@ -65,13 +72,16 @@ public class ExamplePlugin extends JavaPlugin {
}
```
</details>
## Installation
DesertWell is available on JitPack. You can browse the Javadocs [here](https://javadoc.jitpack.io/net/william278/DesertWell/latest/javadoc/).
Note that your plugin will also need to shade (or shade a library that includes) [MineDown](https://github.com/Phoenix616/MineDown).
### Maven
<details>
<summary>Maven</summary>
To include the library with Maven, in your `pom.xml` file, first add the JitPack repository:
```xml
<repositories>
@ -91,6 +101,7 @@ Then, add the dependency in your `<dependencies>` section. Remember to replace `
<scope>compile</scope>
</dependency>
```
</details>
### Gradle & others
JitPack has a [handy guide](https://jitpack.io/#net.william278/DesertWell/#How_to) for how to use the dependency with other build platforms.

@ -1,11 +1,11 @@
plugins {
id 'java'
id 'maven-publish'
id 'com.github.johnrengelman.shadow' version '6.1.0'
id 'com.github.johnrengelman.shadow' version '8.1.1'
}
group 'net.william278'
version '1.1.1'
version '2.0'
repositories {
mavenCentral()
@ -13,7 +13,9 @@ repositories {
}
dependencies {
compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'org.json:json:20230227'
compileOnly 'net.kyori:adventure-api:4.13.1'
compileOnly 'org.jetbrains:annotations:23.0.0'
testImplementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
@ -24,7 +26,7 @@ dependencies {
compileJava.options.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
}

Binary file not shown.

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

10
gradlew vendored

@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -32,10 +32,10 @@
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#

@ -1,80 +0,0 @@
package net.william278.desertwell;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
/**
* Utility for comparing a {@link Version} against the latest version on SpigotMC
*/
@SuppressWarnings("unused")
public class UpdateChecker {
// The SpigotMC.org website API endpoint
private static final String SPIGOT_API_ENDPOINT = "https://api.spigotmc.org/legacy/update.php?resource=";
@NotNull
private final Version currentVersion;
private int resourceId;
private UpdateChecker() {
this.currentVersion = new Version();
}
private UpdateChecker(@NotNull Version currentVersion, final int resourceId) {
this.currentVersion = currentVersion;
this.resourceId = resourceId;
}
/**
* Create a new UpdateChecker for a plugin
*
* @param currentVersion The current version of the plugin
* @param resourceId The resource ID of the plugin on SpigotMC
* @return The {@link UpdateChecker}
*/
public static UpdateChecker create(@NotNull Version currentVersion, final int resourceId) {
return new UpdateChecker(currentVersion, resourceId);
}
/**
* Return the current plugin {@link Version}
*
* @return The current plugin {@link Version}
*/
@NotNull
public Version getCurrentVersion() {
return currentVersion;
}
/**
* Query SpigotMC for the latest {@link Version} of the plugin
*
* @return A {@link CompletableFuture} containing the latest {@link Version} of the plugin
*/
public CompletableFuture<Version> getLatestVersion() {
return CompletableFuture.supplyAsync(() -> {
try (final InputStreamReader inputStreamReader = new InputStreamReader(
new URL(SPIGOT_API_ENDPOINT + resourceId).openConnection().getInputStream())) {
return Version.fromString(new BufferedReader(inputStreamReader).readLine());
} catch (IOException e) {
throw new IllegalStateException("Unable to fetch latest version", e);
}
}).exceptionally(throwable -> {
throwable.printStackTrace();
return new Version();
});
}
/**
* Check if the current plugin {@link Version} is outdated compared to {@link #getLatestVersion()}
*
* @return A {@link CompletableFuture} containing true if the current plugin {@link Version} is outdated
*/
public CompletableFuture<Boolean> isUpToDate() {
return getLatestVersion().thenApply(latestVersion -> currentVersion.compareTo(latestVersion) >= 0);
}
}

@ -1,7 +1,9 @@
package net.william278.desertwell;
package net.william278.desertwell.about;
import de.themoep.minedown.adventure.MineDown;
import de.themoep.minedown.adventure.Util;
import net.kyori.adventure.text.Component;
import net.william278.desertwell.util.Version;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -12,89 +14,24 @@ import java.util.*;
*/
@SuppressWarnings("unused")
public class AboutMenu {
@NotNull
private final String title;
@Nullable
private Version version;
@Nullable
private String description;
@NotNull
private final Version version;
private final String description;
private final Map<String, List<Credit>> attributions;
@NotNull
private final List<Link> buttons;
private AboutMenu() {
this.title = "";
this.buttons = new ArrayList<>();
this.attributions = new LinkedHashMap<>();
}
private AboutMenu(@NotNull String title) {
private AboutMenu(@NotNull String title, @Nullable Version version, @Nullable String description,
@NotNull Map<String, List<Credit>> attributions, @NotNull List<Link> buttons) {
this.title = title;
this.buttons = new ArrayList<>();
this.attributions = new LinkedHashMap<>();
}
/**
* Create an about menu
*
* @param title The title of the menu (resource title)
* @return The {@link AboutMenu}
*/
@NotNull
public static AboutMenu create(@NotNull String title) {
return new AboutMenu(title);
}
/**
* Set the description of the resource to display on the menu
*
* @param description The resource description
* @return The {@link AboutMenu}
*/
@NotNull
public AboutMenu withDescription(@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 AboutMenu}
*/
@NotNull
public AboutMenu withVersion(@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 AboutMenu}
*/
@NotNull
public AboutMenu addAttribution(@NotNull String category, @NotNull Credit... credits) {
final List<Credit> creditList = new ArrayList<>(Arrays.asList(credits));
attributions.putIfAbsent(category, new ArrayList<>());
attributions.get(category).addAll(creditList);
return this;
this.description = description;
this.attributions = attributions;
this.buttons = buttons;
}
/**
* Add linked buttons to the menu
*
* @param links {@link Link}s to add
* @return The {@link AboutMenu}
*/
@NotNull
public AboutMenu addButtons(@NotNull Link... links) {
buttons.addAll(Arrays.asList(links));
return this;
public static Builder builder() {
return new Builder();
}
/**
@ -103,7 +40,7 @@ public class AboutMenu {
* @return The {@link MineDown} menu
*/
@NotNull
public MineDown toMineDown() {
public MineDown asMineDown() {
final StringJoiner menu = new StringJoiner("\n")
.add("[" + escapeMineDown(title) + "](#00fb9a bold)"
+ (version != null ? " [| v" + escapeMineDown(version.toString()) + "](#00fb9a)" : ""));
@ -141,14 +78,18 @@ public class AboutMenu {
return new MineDown(menu.toString()).replace();
}
@NotNull
public Component asComponent() {
return asMineDown().toComponent();
}
/**
* Return the plaintext string formatted menu. Use this for displaying to console
* Return the plaintext string formatted menu.
*
* @return The plaintext menu as a string
*/
@Override
@NotNull
public String toString() {
public String asString() {
final StringJoiner menu = new StringJoiner("\n")
.add(title + (version != null ? " | Version " + version : ""));
if (description != null) {
@ -179,6 +120,121 @@ public class AboutMenu {
return menu.toString();
}
/**
* Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
* <p>
* 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<String, List<Credit>> attributions = new LinkedHashMap<>();
private final List<Link> 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<Credit> 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
* <p>
* 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();
}
}

@ -0,0 +1,19 @@
package net.william278.desertwell.util;
import java.util.function.Consumer;
@FunctionalInterface
public interface ThrowingConsumer<T> extends Consumer<T> {
@Override
default void accept(final T elem) {
try {
acceptThrows(elem);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
void acceptThrows(T elem) throws Exception;
}

@ -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<Completed> 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<String, String> queryFunction;
Endpoint(@NotNull Function<String, String> 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;
}
}
}

@ -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<Version> {
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<Version> {
*
* @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);
}
/**

@ -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());
}
}

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

Loading…
Cancel
Save