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.
This commit is contained in:
1 parent
05b4a2ab85
commit
62de32ecfc
18 files changed
+226
-114
No files matched your search
@@ -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`.
|
||||
|
||||
@@ -217,14 +217,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20250517</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.14.0</version>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.21.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -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<String, GuildService> 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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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<? extends PersistableTask> type = typesById.get(typeId);
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("No persistable task type registered for id: " + typeId);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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> T fromJsonString(@NotNull String json, @NotNull Type targetType) {
|
||||
return GSON.fromJson(json, targetType);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
+23
-7
@@ -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<String, String> 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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user