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
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user