From 62de32ecfccd453274deaa1b3429efe00d9a1549 Mon Sep 17 00:00:00 2001 From: anthorob Date: Fri, 25 Sep 2026 08:18:43 -0400 Subject: [PATCH] Migrate from Gson and org.json to Jackson Replaced Gson and org.json usage across the codebase with a unified Jackson-based implementation through a shared `Json` utility. Updated all JSON serialization and deserialization logic (`GsonJsonMapper`, HTTP client, task persistence, command handling, tests) to use Jackson's `ObjectMapper`. Removed the outdated dependencies from `pom.xml` and verified symmetry in payload serialization. --- .junie/plans/consolidate-json-on-jackson.md | 10 +- pom.xml | 11 +-- .../netneutrality/bootstrap/BotState.java | 19 ++-- .../command/crypto/CryptoPriceCommand.java | 24 +++-- .../core/tasks/PersistableTask.java | 4 +- .../tasks/impl/AbstractPersistableTask.java | 19 +++- .../core/tasks/impl/EmojiTask.java | 37 ++++--- .../persistence/PersistableTaskRegistry.java | 4 +- .../netneutrality/db/service/TaskService.java | 11 ++- .../bobert/netneutrality/http/WebModule.java | 16 +-- .../http/server/GsonJsonMapper.java | 33 ------- .../netneutrality/http/server/WebApi.java | 1 - .../me/bobert/netneutrality/utils/Json.java | 99 +++++++++++++++++++ .../bobert/netneutrality/utils/Settings.java | 8 +- .../core/tasks/TaskManagerImplTest.java | 2 +- .../PersistableTaskRegistryTest.java | 8 +- .../netneutrality/http/WebModuleTest.java | 4 +- .../EmojiReactionRoleTaskPersistenceTest.java | 30 ++++-- 18 files changed, 226 insertions(+), 114 deletions(-) delete mode 100644 src/main/java/me/bobert/netneutrality/http/server/GsonJsonMapper.java create mode 100644 src/main/java/me/bobert/netneutrality/utils/Json.java diff --git a/.junie/plans/consolidate-json-on-jackson.md b/.junie/plans/consolidate-json-on-jackson.md index e8eafd3..23dee58 100644 --- a/.junie/plans/consolidate-json-on-jackson.md +++ b/.junie/plans/consolidate-json-on-jackson.md @@ -217,7 +217,7 @@ graph LR # Delivery Steps -### * Step 1: Introduce Jackson and the shared Json utility +### ✓ Step 1: Introduce Jackson and the shared Json utility Jackson is an explicit dependency and a single shared `ObjectMapper` is available to the codebase. - Add `com.fasterxml.jackson.core:jackson-databind` to `pom.xml`, pinned to the version already resolved via JDA (2.21.2), next to the existing `org.json`/`gson` entries (which stay for now so the build keeps compiling). @@ -225,14 +225,14 @@ Jackson is an explicit dependency and a single shared `ObjectMapper` is availabl - Have `readObject`/`readArray` wrap Jackson's checked `JsonProcessingException` in an unchecked exception so existing call-site signatures stay unchanged. - Verify no shade-plugin relocation or filter rules in `pom.xml` reference `org.json` or `gson`. -### Step 2: Migrate the Gson call sites to Jackson +### ✓ Step 2: Migrate the Gson call sites to Jackson Gson is no longer referenced anywhere in the source tree. - Delete `http/server/GsonJsonMapper.java` and remove its registration from the Javalin configuration, relying on Javalin 7's built-in Jackson `JsonMapper` instead. - Replace `new Gson().toJson(params)` at `http/WebModule.java:256` with `Json.write(params)`, reusing the shared mapper instead of allocating per request. - Confirm Javalin request/response serialization still behaves identically for the existing endpoints. -### Step 3: Migrate the HTTP client and its consumers +### ✓ Step 3: Migrate the HTTP client and its consumers `WebModule` returns Jackson nodes and its callers read them. - Change `WebModule.getAsObject()` to return `ObjectNode` and `getAsArray()` to return `ArrayNode` (lines 133-143), parsing through `Json`. @@ -240,7 +240,7 @@ Gson is no longer referenced anywhere in the source tree. - Update `command/crypto/CryptoPriceCommand.java` to read the crypto API response via `JsonNode` accessors. - Remove the now-unused `org.json` imports from these three files. -### Step 4: Migrate the task persistence layer +### ✓ Step 4: Migrate the task persistence layer Tasks serialize and restore through Jackson while keeping the existing on-disk payload shape. - Change `PersistableTask.restore(JSONObject)` to `restore(ObjectNode)`. @@ -249,7 +249,7 @@ Tasks serialize and restore through Jackson while keeping the existing on-disk p - Update `PersistableTaskRegistry` and `db/service/TaskService` to parse stored payload strings via `Json.readObject`. - Confirm `tasks/reaction/EmojiReactionRoleTask` still compiles unchanged against the new `EmojiTask` contract. -### Step 5: Update tests and drop the old dependencies +### ✓ Step 5: Update tests and drop the old dependencies `org.json` and Gson are removed from `pom.xml` and the full build passes. - Rewrite payload construction in `PersistableTaskRegistryTest` and `EmojiReactionRoleTaskPersistenceTest` using `ObjectNode`. diff --git a/pom.xml b/pom.xml index a4e0c8e..f7d1422 100644 --- a/pom.xml +++ b/pom.xml @@ -217,14 +217,9 @@ - org.json - json - 20250517 - - - com.google.code.gson - gson - 2.14.0 + com.fasterxml.jackson.core + jackson-databind + 2.21.2 diff --git a/src/main/java/me/bobert/netneutrality/bootstrap/BotState.java b/src/main/java/me/bobert/netneutrality/bootstrap/BotState.java index 873d59d..81840ea 100644 --- a/src/main/java/me/bobert/netneutrality/bootstrap/BotState.java +++ b/src/main/java/me/bobert/netneutrality/bootstrap/BotState.java @@ -5,13 +5,14 @@ package me.bobert.netneutrality.bootstrap; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.db.service.GuildServiceProvider; import me.bobert.netneutrality.db.service.GuildService; import me.bobert.netneutrality.music.MusicService; import me.bobert.netneutrality.music.MusicManager; import me.bobert.netneutrality.music.TrackScheduler; -import org.json.JSONArray; -import org.json.JSONObject; +import me.bobert.netneutrality.utils.Json; import java.io.File; import java.io.FileWriter; @@ -43,8 +44,8 @@ public class BotState { Map services = guildServices.getServices(); - JSONObject main = new JSONObject(); - JSONArray guildsArray = new JSONArray(); + ObjectNode main = Json.object(); + ArrayNode guildsArray = Json.array(); for (MusicManager manager : musicService.getManagers()) { TrackScheduler trackScheduler = manager.getScheduler(); @@ -53,8 +54,8 @@ public class BotState { for (GuildService service : services.values()) { - JSONObject guild = new JSONObject(); - JSONObject music = new JSONObject(); + ObjectNode guild = Json.object(); + ObjectNode music = Json.object(); guild.put("id", service.getEntity().getGuildId()); // MessageChannel currentChannel = trackScheduler.getTextChannel(); @@ -64,12 +65,12 @@ public class BotState { // music.put("current_channel", currentChannel.getId()); // } - guild.put("music", music); + guild.set("music", music); - guildsArray.put(guild); + guildsArray.add(guild); } - main.put("guilds", guildsArray); + main.set("guilds", guildsArray); File state = new File("state.json"); try { diff --git a/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java b/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java index 24f9ee2..2597d63 100644 --- a/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java +++ b/src/main/java/me/bobert/netneutrality/command/crypto/CryptoPriceCommand.java @@ -5,6 +5,8 @@ package me.bobert.netneutrality.command.crypto; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.core.LogHandler; import me.bobert.netneutrality.core.command.CommandCategory; import me.bobert.netneutrality.core.command.CommandExecutionResult; @@ -15,7 +17,6 @@ import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.http.WebModule; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; -import org.json.JSONObject; import java.text.NumberFormat; import java.util.Currency; @@ -91,14 +92,19 @@ public class CryptoPriceCommand extends AbstractCommand { } WebModule http = new WebModule.Builder(String.format(baseURL, symbol, finalCurrency)).cache(30, TimeUnit.MINUTES).build().request(); - JSONObject json = http.getAsObject(); + ObjectNode json = http.getAsObject(); - if (json.isEmpty() || (json.has("Response") && json.getString("Response").equalsIgnoreCase("Error"))) { + if (json.isEmpty() || json.path("Response").asText("").equalsIgnoreCase("Error")) { asyncMessage.edit("Crypto doesn't exist.. yet"); return; } - double value = json.getDouble(finalCurrency); + JsonNode priceNode = json.path(finalCurrency); + if (!priceNode.isNumber()) { + throw new IllegalStateException("Missing price for currency " + finalCurrency); + } + + double value = priceNode.asDouble(); String price = dollarFormat.format(value); asyncMessage.edit(String.format("The current price of %s in %s is %s", symbol, finalCurrency, price)); @@ -115,12 +121,12 @@ public class CryptoPriceCommand extends AbstractCommand { private boolean validateSymbol(String symbol) { try { WebModule http = new WebModule.Builder(exchangeApi).get().cache(1, TimeUnit.DAYS).build().request(); - JSONObject json = http.getAsObject(); + ObjectNode json = http.getAsObject(); - JSONObject pairs = json.getJSONObject("Data") - .getJSONObject("exchanges") - .getJSONObject(exchange) - .getJSONObject("pairs"); + JsonNode pairs = json.path("Data") + .path("exchanges") + .path(exchange) + .path("pairs"); return pairs.has(symbol); diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java index 038a5e6..75e87e0 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/PersistableTask.java @@ -5,8 +5,8 @@ package me.bobert.netneutrality.core.tasks; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.core.context.CommandContext; -import org.json.JSONObject; /** * Represents a task that can be persisted and restored later. @@ -29,7 +29,7 @@ public interface PersistableTask extends Task { * * @param payload the persisted payload, must not be {@code null} */ - void restore(JSONObject payload); + void restore(ObjectNode payload); void insert(CommandContext context); diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/AbstractPersistableTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/AbstractPersistableTask.java index f1ef9e0..dfaa45a 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/impl/AbstractPersistableTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/AbstractPersistableTask.java @@ -5,10 +5,11 @@ package me.bobert.netneutrality.core.tasks.impl; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.core.context.CommandContext; import me.bobert.netneutrality.core.tasks.PersistableTask; import me.bobert.netneutrality.core.tasks.TaskManager; -import org.json.JSONObject; +import me.bobert.netneutrality.utils.Json; public abstract class AbstractPersistableTask extends AbstractTask implements PersistableTask { @@ -66,15 +67,27 @@ public abstract class AbstractPersistableTask extends AbstractTask implements Pe * @param payload the persisted payload */ @Override - public void restore(JSONObject payload) { + public void restore(ObjectNode payload) { // Nothing to restore by default } + /** + * No-op default: only tasks carrying extra state need to override this. + * + * @param payload the payload being written + */ + protected void writePayload(ObjectNode payload) { + // Nothing extra to write by default + } + @Override public String toPayload() { - JSONObject payload = new JSONObject(this); + ObjectNode payload = Json.object(); payload.put("id", getId()); payload.put("lockId", getLockId()); + payload.put("name", getName()); + + writePayload(payload); return payload.toString(); } diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java index bab1859..8c070f4 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/impl/EmojiTask.java @@ -5,9 +5,10 @@ package me.bobert.netneutrality.core.tasks.impl; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.context.tasks.EmojiTaskContext; import me.bobert.netneutrality.core.tasks.TaskManager; -import org.json.JSONObject; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,7 +28,7 @@ public abstract class EmojiTask extends AbstractPersistableTask { /** * Constructor used by the persistable task registry: the message coordinates and the emoji - * mapping are hydrated afterwards by {@link #restore(JSONObject)}. + * mapping are hydrated afterwards by {@link #restore(ObjectNode)}. * * @param taskManager the application task manager, must not be {@code null} */ @@ -36,17 +37,31 @@ public abstract class EmojiTask extends AbstractPersistableTask { } @Override - public void restore(JSONObject payload) { - this.guildId = payload.optString("guildId", null); - this.channelId = payload.optString("channelId", null); - this.messageId = payload.optString("messageId", null); + public void restore(ObjectNode payload) { + this.guildId = payload.path("guildId").asText(null); + this.channelId = payload.path("channelId").asText(null); + this.messageId = payload.path("messageId").asText(null); emojiMapping.clear(); - JSONObject mapping = payload.optJSONObject("emojiMapping"); - if (mapping != null) { - for (String emoji : mapping.keySet()) { - emojiMapping.put(emoji, mapping.getString(emoji)); - } + JsonNode mapping = payload.path("emojiMapping"); + if (mapping.isObject()) { + mapping.properties().forEach(entry -> emojiMapping.put(entry.getKey(), entry.getValue().asText())); + } + } + + @Override + protected void writePayload(ObjectNode payload) { + putIfPresent(payload, "guildId", guildId); + putIfPresent(payload, "channelId", channelId); + putIfPresent(payload, "messageId", messageId); + + ObjectNode mapping = payload.putObject("emojiMapping"); + emojiMapping.forEach(mapping::put); + } + + private static void putIfPresent(ObjectNode payload, String key, String value) { + if (value != null) { + payload.put(key, value); } } diff --git a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistry.java b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistry.java index d9e71f6..02a58f3 100644 --- a/src/main/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistry.java +++ b/src/main/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistry.java @@ -5,12 +5,12 @@ package me.bobert.netneutrality.core.tasks.persistence; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.bootstrap.ApplicationContext; import me.bobert.netneutrality.bootstrap.ReflectionHelper; import me.bobert.netneutrality.core.tasks.PersistableTask; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.json.JSONObject; import java.util.Collections; import java.util.Map; @@ -88,7 +88,7 @@ public final class PersistableTaskRegistry { * @throws IllegalArgumentException if no task type is registered under {@code typeId} * @throws ReflectiveOperationException if the task type cannot be instantiated */ - public PersistableTask create(String typeId, JSONObject payload) throws ReflectiveOperationException { + public PersistableTask create(String typeId, ObjectNode payload) throws ReflectiveOperationException { Class type = typesById.get(typeId); if (type == null) { throw new IllegalArgumentException("No persistable task type registered for id: " + typeId); diff --git a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java index b705323..4734426 100644 --- a/src/main/java/me/bobert/netneutrality/db/service/TaskService.java +++ b/src/main/java/me/bobert/netneutrality/db/service/TaskService.java @@ -5,14 +5,15 @@ package me.bobert.netneutrality.db.service; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.core.tasks.PersistableTask; import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.core.tasks.persistence.PersistableTaskRegistry; import me.bobert.netneutrality.db.entities.OTask; import me.bobert.netneutrality.db.repository.TaskRepository; +import me.bobert.netneutrality.utils.Json; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.json.JSONObject; import java.util.List; import java.util.Objects; @@ -42,7 +43,7 @@ public class TaskService { String payload = oTask.getPayload(); try { - JSONObject json = new JSONObject(payload); + ObjectNode json = Json.readObject(payload); PersistableTask task = registry.create(typeId, json); validateTask(task, oTask); @@ -61,13 +62,13 @@ public class TaskService { throw new IllegalArgumentException("Task ID mismatch, expected: " + task.getId()); } - JSONObject json = new JSONObject(payload); - String payloadId = json.getString("id"); + ObjectNode json = Json.readObject(payload); + String payloadId = json.path("id").asText(null); if (!Objects.equals(payloadId, task.getId())) { throw new IllegalArgumentException("Task payload ID mismatch, expected: " + task.getId()); } - String payloadLockId = json.getString("lockId"); + String payloadLockId = json.path("lockId").asText(null); if (!Objects.equals(payloadLockId, task.getLockId())) { throw new IllegalArgumentException("Task lock ID mismatch, expected: " + task.getLockId()); } diff --git a/src/main/java/me/bobert/netneutrality/http/WebModule.java b/src/main/java/me/bobert/netneutrality/http/WebModule.java index 6505694..6042e7a 100644 --- a/src/main/java/me/bobert/netneutrality/http/WebModule.java +++ b/src/main/java/me/bobert/netneutrality/http/WebModule.java @@ -6,17 +6,17 @@ package me.bobert.netneutrality.http; * https://discord.gg/qUa6zNFZsd */ -import com.google.gson.Gson; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.sentry.Breadcrumb; import io.sentry.Sentry; import me.bobert.netneutrality.http.ResponseCache.CachedResponse; +import me.bobert.netneutrality.utils.Json; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import org.json.JSONArray; -import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -130,15 +130,15 @@ public class WebModule { return keyBuilder.toString(); } - public JSONObject getAsObject() { + public ObjectNode getAsObject() { return cachedResponse.getBody() - .map(JSONObject::new) + .map(Json::readObject) .orElseThrow(() -> new IllegalStateException("No body available for this response")); } - public JSONArray getAsArray() { + public ArrayNode getAsArray() { return cachedResponse.getBody() - .map(JSONArray::new) + .map(Json::readArray) .orElseThrow(() -> new IllegalStateException("No body available for this response")); } @@ -253,7 +253,7 @@ public class WebModule { } if (method == Method.POST && !params.isEmpty()) { - RequestBody body = RequestBody.create(new Gson().toJson(params), MediaType.parse("application/json; charset=utf-8")); + RequestBody body = RequestBody.create(Json.write(params), MediaType.parse("application/json; charset=utf-8")); builder.post(body); } diff --git a/src/main/java/me/bobert/netneutrality/http/server/GsonJsonMapper.java b/src/main/java/me/bobert/netneutrality/http/server/GsonJsonMapper.java deleted file mode 100644 index cbfa38e..0000000 --- a/src/main/java/me/bobert/netneutrality/http/server/GsonJsonMapper.java +++ /dev/null @@ -1,33 +0,0 @@ -package me.bobert.netneutrality.http.server; - -/* - * Copyright (c) anthorob - * - * https://discord.gg/qUa6zNFZsd - */ - -import com.google.gson.Gson; -import io.javalin.json.JsonMapper; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Type; - -/** - * Javalin {@link JsonMapper} backed by a shared {@link Gson} instance. - */ -public class GsonJsonMapper implements JsonMapper { - - private static final Gson GSON = new Gson(); - - @NotNull - @Override - public String toJsonString(@NotNull Object obj, @NotNull Type type) { - return GSON.toJson(obj, type); - } - - @NotNull - @Override - public T fromJsonString(@NotNull String json, @NotNull Type targetType) { - return GSON.fromJson(json, targetType); - } -} diff --git a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java index 0a3fbf1..06f418f 100644 --- a/src/main/java/me/bobert/netneutrality/http/server/WebApi.java +++ b/src/main/java/me/bobert/netneutrality/http/server/WebApi.java @@ -57,7 +57,6 @@ public class WebApi implements Lifecycle { // Javalin 7 requires routes, exception and error handlers to be registered upfront. app = Javalin.create(config -> { - config.jsonMapper(new GsonJsonMapper()); config.startup.showJavalinBanner = false; config.routes.get("/", ctx -> ctx.html(LIVENESS_HTML)); diff --git a/src/main/java/me/bobert/netneutrality/utils/Json.java b/src/main/java/me/bobert/netneutrality/utils/Json.java new file mode 100644 index 0000000..961515d --- /dev/null +++ b/src/main/java/me/bobert/netneutrality/utils/Json.java @@ -0,0 +1,99 @@ +package me.bobert.netneutrality.utils; + +/* + * Copyright (c) anthorob + * + * https://discord.gg/qUa6zNFZsd + */ + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Process-wide JSON entry point backed by a single shared {@link ObjectMapper}. + */ +public final class Json { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private Json() { + } + + public static ObjectMapper mapper() { + return MAPPER; + } + + public static ObjectNode object() { + return MAPPER.createObjectNode(); + } + + public static ArrayNode array() { + return MAPPER.createArrayNode(); + } + + /** + * Parses a JSON document expected to be an object. + * + * @param json the raw JSON document + * @return the parsed object node + * @throws JsonException if the document is malformed or is not an object + */ + public static ObjectNode readObject(String json) { + JsonNode node = read(json); + + if (!(node instanceof ObjectNode objectNode)) { + throw new JsonException("Expected a JSON object but got: " + node.getNodeType()); + } + + return objectNode; + } + + /** + * Parses a JSON document expected to be an array. + * + * @param json the raw JSON document + * @return the parsed array node + * @throws JsonException if the document is malformed or is not an array + */ + public static ArrayNode readArray(String json) { + JsonNode node = read(json); + + if (!(node instanceof ArrayNode arrayNode)) { + throw new JsonException("Expected a JSON array but got: " + node.getNodeType()); + } + + return arrayNode; + } + + public static JsonNode read(String json) { + try { + return MAPPER.readTree(json); + } catch (JsonProcessingException e) { + throw new JsonException("Cannot parse JSON document", e); + } + } + + public static String write(Object value) { + try { + return MAPPER.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new JsonException("Cannot serialize value to JSON", e); + } + } + + /** + * Unchecked wrapper around Jackson's checked processing exceptions. + */ + public static class JsonException extends RuntimeException { + public JsonException(String message) { + super(message); + } + + public JsonException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/me/bobert/netneutrality/utils/Settings.java b/src/main/java/me/bobert/netneutrality/utils/Settings.java index b429859..2e33bfb 100644 --- a/src/main/java/me/bobert/netneutrality/utils/Settings.java +++ b/src/main/java/me/bobert/netneutrality/utils/Settings.java @@ -6,9 +6,9 @@ package me.bobert.netneutrality.utils; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ArrayNode; import me.bobert.netneutrality.http.WebModule; import org.apache.logging.log4j.LogManager; -import org.json.JSONArray; import java.io.File; import java.io.FileInputStream; @@ -101,11 +101,11 @@ public class Settings implements SettingsProvider { String gitlabProjectId = "22"; String latestReleaseUrl = String.format("%s/api/v4/projects/%s/releases/", gitlabHost, gitlabProjectId); - JSONArray json = new WebModule.Builder(latestReleaseUrl).cache(1, TimeUnit.DAYS).build().request().getAsArray(); - if (json.isNull(0)) { + ArrayNode json = new WebModule.Builder(latestReleaseUrl).cache(1, TimeUnit.DAYS).build().request().getAsArray(); + if (json.isEmpty() || !json.get(0).hasNonNull("tag_name")) { return "0.0.0"; } - return json.getJSONObject(0).getString("tag_name"); + return json.get(0).get("tag_name").asText(); } } diff --git a/src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java b/src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java index f905b61..0fcd9c7 100644 --- a/src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java +++ b/src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java @@ -214,7 +214,7 @@ class TaskManagerImplTest { } @Override - public void restore(org.json.JSONObject payload) { + public void restore(com.fasterxml.jackson.databind.node.ObjectNode payload) { } @Override diff --git a/src/test/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistryTest.java b/src/test/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistryTest.java index c70e5be..228681a 100644 --- a/src/test/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistryTest.java +++ b/src/test/java/me/bobert/netneutrality/core/tasks/persistence/PersistableTaskRegistryTest.java @@ -12,7 +12,7 @@ import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.core.tasks.impl.AbstractPersistableTask; import me.bobert.netneutrality.core.tasks.impl.EmojiTask; import me.bobert.netneutrality.tasks.reaction.EmojiReactionRoleTask; -import org.json.JSONObject; +import me.bobert.netneutrality.utils.Json; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,7 +49,7 @@ class PersistableTaskRegistryTest { registry.scan(); for (String typeId : registry.getTypeIds()) { - Class type = registry.create(typeId, new JSONObject()).getClass(); + Class type = registry.create(typeId, Json.object()).getClass(); assertFalse(Modifier.isAbstract(type.getModifiers()), type.getName() + " must not be abstract"); assertNotEquals(AbstractPersistableTask.class, type); @@ -61,7 +61,7 @@ class PersistableTaskRegistryTest { void createInstantiatesRegisteredTypeThroughDi() throws Exception { registry.scan(); - PersistableTask task = registry.create("emoji-reaction-role", new JSONObject()); + PersistableTask task = registry.create("emoji-reaction-role", Json.object()); assertInstanceOf(EmojiReactionRoleTask.class, task); } @@ -70,7 +70,7 @@ class PersistableTaskRegistryTest { void createRejectsUnknownTypeId() { registry.scan(); - assertThrows(IllegalArgumentException.class, () -> registry.create("does-not-exist", new JSONObject())); + assertThrows(IllegalArgumentException.class, () -> registry.create("does-not-exist", Json.object())); } @Test diff --git a/src/test/java/me/bobert/netneutrality/http/WebModuleTest.java b/src/test/java/me/bobert/netneutrality/http/WebModuleTest.java index 221eb39..6e23bb5 100644 --- a/src/test/java/me/bobert/netneutrality/http/WebModuleTest.java +++ b/src/test/java/me/bobert/netneutrality/http/WebModuleTest.java @@ -45,9 +45,9 @@ class WebModuleTest { String url = url("cache-hit"); assertEquals(1, new WebModule.Builder(url).get().cache(1, TimeUnit.MINUTES).build().request() - .getAsObject().getInt("hits")); + .getAsObject().get("hits").asInt()); assertEquals(1, new WebModule.Builder(url).get().cache(1, TimeUnit.MINUTES).build().request() - .getAsObject().getInt("hits")); + .getAsObject().get("hits").asInt()); assertEquals(1, hits.get()); } diff --git a/src/test/java/me/bobert/netneutrality/tasks/reaction/EmojiReactionRoleTaskPersistenceTest.java b/src/test/java/me/bobert/netneutrality/tasks/reaction/EmojiReactionRoleTaskPersistenceTest.java index 7ae7461..734ca77 100644 --- a/src/test/java/me/bobert/netneutrality/tasks/reaction/EmojiReactionRoleTaskPersistenceTest.java +++ b/src/test/java/me/bobert/netneutrality/tasks/reaction/EmojiReactionRoleTaskPersistenceTest.java @@ -5,12 +5,13 @@ package me.bobert.netneutrality.tasks.reaction; * https://discord.gg/qUa6zNFZsd */ +import com.fasterxml.jackson.databind.node.ObjectNode; import me.bobert.netneutrality.bootstrap.ApplicationContext; import me.bobert.netneutrality.core.tasks.TaskManager; import me.bobert.netneutrality.core.tasks.impl.EmojiTask; import me.bobert.netneutrality.core.tasks.persistence.PersistableTaskRegistry; import me.bobert.netneutrality.db.service.TaskService; -import org.json.JSONObject; +import me.bobert.netneutrality.utils.Json; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,7 +46,7 @@ class EmojiReactionRoleTaskPersistenceTest { Map mapping = Map.of("\uD83D\uDD25", "role-1", "\u2705", "role-2"); EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1", mapping); - JSONObject payload = new JSONObject(original.toPayload()); + ObjectNode payload = Json.readObject(original.toPayload()); EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload)); assertEquals(original.getId(), restored.getId()); @@ -68,17 +69,17 @@ class EmojiReactionRoleTaskPersistenceTest { void payloadAlwaysCarriesIdAndLockId() { EmojiReactionRoleTask task = new EmojiReactionRoleTask(taskManager, "g", "c", "m"); - JSONObject payload = new JSONObject(task.toPayload()); + ObjectNode payload = Json.readObject(task.toPayload()); - assertEquals(task.getId(), payload.getString("id")); - assertEquals(task.getLockId(), payload.getString("lockId")); + assertEquals(task.getId(), payload.get("id").asText()); + assertEquals(task.getLockId(), payload.get("lockId").asText()); } @Test void missingEmojiMappingRestoresToEmptyMap() throws Exception { EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1"); - JSONObject payload = new JSONObject(original.toPayload()); + ObjectNode payload = Json.readObject(original.toPayload()); payload.remove("emojiMapping"); EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload)); @@ -90,7 +91,7 @@ class EmojiReactionRoleTaskPersistenceTest { void restoredTaskHasInjectedTaskManager() throws Exception { EmojiReactionRoleTask original = new EmojiReactionRoleTask(taskManager, "guild-1", "channel-1", "message-1"); - JSONObject payload = new JSONObject(original.toPayload()); + ObjectNode payload = Json.readObject(original.toPayload()); EmojiTask restored = assertInstanceOf(EmojiTask.class, registry.create(original.typeId(), payload)); restored.addEmoji("\uD83D\uDD25", "role-1"); @@ -98,4 +99,19 @@ class EmojiReactionRoleTaskPersistenceTest { assertEquals("role-1", restored.getEmojiMapping().get("\uD83D\uDD25")); verify(taskService).persistTask(restored); } + + @Test + void legacyPayloadStillRestores() throws Exception { + String legacy = "{\"id\":\"emoji-reaction-role-guild-1-message-1\",\"lockId\":\"lock-1\",\"name\":\"emoji-reaction-role\"," + + "\"guildId\":\"guild-1\",\"channelId\":\"channel-1\",\"messageId\":\"message-1\"," + + "\"emojiMapping\":{\"\uD83D\uDD25\":\"role-1\"}}"; + + EmojiTask restored = assertInstanceOf(EmojiTask.class, + registry.create("emoji-reaction-role", Json.readObject(legacy))); + + assertEquals("guild-1", restored.getGuildId()); + assertEquals("channel-1", restored.getChannelId()); + assertEquals("message-1", restored.getMessageId()); + assertEquals(Map.of("\uD83D\uDD25", "role-1"), restored.getEmojiMapping()); + } }