From 4ec47b5e4bdc7822cdc2ccaa3a8f3c0c3c3789ce Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Thu, 16 Mar 2023 22:34:52 +0100 Subject: [PATCH] Fix Gson unable to (de)serialize Throwable instance --- .../java/fr/pandacube/lib/core/json/Json.java | 2 + .../core/json/StackTraceElementAdapter.java | 64 ++++++ .../lib/core/json/ThrowableAdapter.java | 197 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 pandalib-core/src/main/java/fr/pandacube/lib/core/json/StackTraceElementAdapter.java create mode 100644 pandalib-core/src/main/java/fr/pandacube/lib/core/json/ThrowableAdapter.java diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java index 37e62fc..30fd33c 100644 --- a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/Json.java @@ -107,6 +107,8 @@ public class Json { static { if (!hasGsonNativeRecordSupport()) registerTypeAdapterFactory(RecordTypeAdapter.FACTORY); + registerTypeAdapterFactory(StackTraceElementAdapter.FACTORY); + registerTypeAdapterFactory(ThrowableAdapter.FACTORY); } } diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/StackTraceElementAdapter.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/StackTraceElementAdapter.java new file mode 100644 index 0000000..3bc5e3c --- /dev/null +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/StackTraceElementAdapter.java @@ -0,0 +1,64 @@ +package fr.pandacube.lib.core.json; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.bind.TreeTypeAdapter; + +import java.lang.reflect.Type; + +/* package */ class StackTraceElementAdapter implements JsonSerializer, JsonDeserializer { + + public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(StackTraceElement.class, new StackTraceElementAdapter()); + + + @Override + public StackTraceElement deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject obj = json.getAsJsonObject(); + + String classLoader = obj.has("classloader") && obj.get("classloader").isJsonPrimitive() + ? obj.get("classloader").getAsString() : null; + String module = obj.has("module") && obj.get("module").isJsonPrimitive() + ? obj.get("module").getAsString() : null; + String moduleVersion = obj.has("moduleversion") && obj.get("moduleversion").isJsonPrimitive() + ? obj.get("moduleversion").getAsString() : null; + String clazz = obj.has("class") && obj.get("class").isJsonPrimitive() + ? obj.get("class").getAsString() : null; + if (clazz == null) { + throw new JsonParseException("Missing 'class' entry"); + } + String method = obj.has("method") && obj.get("method").isJsonPrimitive() + ? obj.get("method").getAsString() : null; + if (method == null) { + throw new JsonParseException("Missing 'method' entry"); + } + String file = obj.has("file") && obj.get("file").isJsonPrimitive() + ? obj.get("file").getAsString() : null; + int line = obj.has("line") && obj.get("line").isJsonPrimitive() + ? obj.get("line").getAsInt() : -1; + + return new StackTraceElement(classLoader, module, moduleVersion, clazz, method, file, line); + } + + @Override + public JsonElement serialize(StackTraceElement src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("class", src.getClassName()); + obj.addProperty("method", src.getMethodName()); + obj.addProperty("line", src.getLineNumber()); + if (src.getClassLoaderName() != null) + obj.addProperty("classloader", src.getClassLoaderName()); + if (src.getModuleName() != null) + obj.addProperty("module", src.getModuleName()); + if (src.getModuleVersion() != null) + obj.addProperty("moduleversion", src.getModuleVersion()); + if (src.getFileName() != null) + obj.addProperty("file", src.getFileName()); + return obj; + } +} diff --git a/pandalib-core/src/main/java/fr/pandacube/lib/core/json/ThrowableAdapter.java b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/ThrowableAdapter.java new file mode 100644 index 0000000..62f00cc --- /dev/null +++ b/pandalib-core/src/main/java/fr/pandacube/lib/core/json/ThrowableAdapter.java @@ -0,0 +1,197 @@ +package fr.pandacube.lib.core.json; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.bind.TreeTypeAdapter; +import com.google.gson.stream.MalformedJsonException; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; +import java.util.function.BiFunction; +import java.util.function.Function; + +/* package */ class ThrowableAdapter implements JsonSerializer, JsonDeserializer { + + public static final TypeAdapterFactory FACTORY = TreeTypeAdapter.newTypeHierarchyFactory(Throwable.class, new ThrowableAdapter()); + + + private static final Map, ThrowableSubAdapter> subAdapters = Collections.synchronizedMap(new HashMap<>()); + + public static void registerSubAdapter(Class clazz, ThrowableSubAdapter subAdapter) { + subAdapters.put(clazz, subAdapter); + } + + @Override + public Throwable deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + + JsonObject obj = json.getAsJsonObject(); + String message = obj.has("message") && !obj.get("message").isJsonNull() + ? obj.get("message").getAsString() : null; + Throwable cause = obj.has("cause") && !obj.get("cause").isJsonNull() + ? context.deserialize(obj.get("cause"), Throwable.class) : null; + + // handle types + Throwable t = null; + if (obj.has("types") && obj.get("types").isJsonArray()) { + for (JsonElement clNameEl : obj.getAsJsonArray("types")) { + String clName = clNameEl.getAsString(); + try { + Class cl = Class.forName(clName); + synchronized (subAdapters) { + if (subAdapters.containsKey(cl)) { + t = subAdapters.get(cl).constructor.apply(message, cause); + break; + } + } + } catch (ReflectiveOperationException ignore) { } + } + } + if (t == null) { + t = new Throwable(message, cause); + } + + // handle suppressed + JsonArray suppressed = obj.has("suppressed") && !obj.get("suppressed").isJsonNull() + ? obj.get("suppressed").getAsJsonArray() : null; + if (suppressed != null) { + for (JsonElement jsonel : suppressed) { + t.addSuppressed(context.deserialize(jsonel, Throwable.class)); + } + } + + // handle stacktrace + JsonArray stacktrace = obj.has("stacktrace") && !obj.get("stacktrace").isJsonNull() + ? obj.get("stacktrace").getAsJsonArray() : null; + if (stacktrace != null) { + List els = new ArrayList<>(); + for (JsonElement jsonel : stacktrace) { + els.add(context.deserialize(jsonel, StackTraceElement.class)); + } + t.setStackTrace(els.toArray(new StackTraceElement[0])); + } + + return t; + } + + @Override + public JsonElement serialize(Throwable src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + + // toString for easy json reading (not used for deserialization) + json.addProperty("tostring", src.toString()); + + // handle types + JsonArray types = new JsonArray(); + Class cl = src.getClass(); + while (cl != Throwable.class) { + if (cl.getCanonicalName() != null) + types.add(cl.getCanonicalName()); + cl = cl.getSuperclass(); + } + json.add("types", types); + + // general data + if (src.getMessage() != null) + json.addProperty("message", src.getMessage()); + if (src.getCause() != null) + json.add("cause", context.serialize(src.getCause())); + + // handle suppressed + JsonArray suppressed = new JsonArray(); + for (Throwable supp : src.getSuppressed()) { + suppressed.add(context.serialize(supp)); + } + json.add("suppressed", suppressed); + + // handle stacktrace + JsonArray stacktrace = new JsonArray(); + for (StackTraceElement stackTraceElement : src.getStackTrace()) { + stacktrace.add(context.serialize(stackTraceElement)); + } + json.add("stacktrace", stacktrace); + + return json; + } + + + + + + public static class ThrowableSubAdapter { + public final BiFunction constructor; + + protected ThrowableSubAdapter(BiFunction constructor) { + this.constructor = constructor; + } + + public static BiFunction messageOnly(Function constructorWithMessage) { + return (m, t) -> { + T inst = constructorWithMessage.apply(m); + try { + inst.initCause(t); + } catch (Exception ignore) { } + return inst; + }; + } + } + + + static { + // java.lang + registerSubAdapter(Throwable.class, new ThrowableSubAdapter<>(Throwable::new)); + registerSubAdapter(Error.class, new ThrowableSubAdapter<>(Error::new)); + registerSubAdapter(OutOfMemoryError.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(OutOfMemoryError::new))); + registerSubAdapter(StackOverflowError.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(StackOverflowError::new))); + registerSubAdapter(Exception.class, new ThrowableSubAdapter<>(Exception::new)); + registerSubAdapter(RuntimeException.class, new ThrowableSubAdapter<>(RuntimeException::new)); + registerSubAdapter(NullPointerException.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(NullPointerException::new))); + registerSubAdapter(IndexOutOfBoundsException.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(IndexOutOfBoundsException::new))); + registerSubAdapter(IllegalArgumentException.class, new ThrowableSubAdapter<>(IllegalArgumentException::new)); + registerSubAdapter(IllegalStateException.class, new ThrowableSubAdapter<>(IllegalStateException::new)); + registerSubAdapter(SecurityException.class, new ThrowableSubAdapter<>(SecurityException::new)); + registerSubAdapter(ReflectiveOperationException.class, new ThrowableSubAdapter<>(ReflectiveOperationException::new)); + registerSubAdapter(UnsupportedOperationException.class, new ThrowableSubAdapter<>(UnsupportedOperationException::new)); + registerSubAdapter(InterruptedException.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(InterruptedException::new))); + + // java.io + registerSubAdapter(IOException.class, new ThrowableSubAdapter<>(IOException::new)); + + // java.sql + registerSubAdapter(SQLException.class, new ThrowableSubAdapter<>(SQLException::new)); + + // java.util + registerSubAdapter(NoSuchElementException.class, new ThrowableSubAdapter<>(NoSuchElementException::new)); + + // java.util.concurrent + registerSubAdapter(CancellationException.class, new ThrowableSubAdapter<>(ThrowableSubAdapter.messageOnly(CancellationException::new))); + registerSubAdapter(ExecutionException.class, new ThrowableSubAdapter<>(ExecutionException::new)); + registerSubAdapter(CompletionException.class, new ThrowableSubAdapter<>(CompletionException::new)); + + // gson + registerSubAdapter(JsonIOException.class, new ThrowableSubAdapter<>(JsonIOException::new)); + registerSubAdapter(JsonParseException.class, new ThrowableSubAdapter<>(JsonParseException::new)); + registerSubAdapter(JsonSyntaxException.class, new ThrowableSubAdapter<>(JsonSyntaxException::new)); + registerSubAdapter(MalformedJsonException.class, new ThrowableSubAdapter<>(MalformedJsonException::new)); + } + + +}