From c75cf037620d2880cb729ef5b99728b3a2513e2f Mon Sep 17 00:00:00 2001 From: Jonah Bauer Date: Wed, 12 Jan 2022 10:28:31 +0100 Subject: [PATCH] migrated to jackson --- build.gradle.kts | 1 - buildSrc/src/main/kotlin/Dependencies.kt | 8 +- .../wizard/client/cli/state/Game.java | 2 +- .../wizard/client/libgdx/state/Game.java | 2 +- wizard-common/build.gradle.kts | 10 + .../RuntimeTypeAdapterFactory.java | 273 ------------------ .../common/messages/ParseException.java | 7 + .../common/messages/client/ClientMessage.java | 20 +- .../messages/observer/ObserverMessage.java | 19 +- .../common/messages/player/PlayerMessage.java | 19 +- .../common/messages/server/ServerMessage.java | 20 +- .../util/SealedClassTypeAdapterFactory.java | 62 ---- .../wizard/common/util/SerializationUtil.java | 87 ++++++ .../server/socket/WizardSocketHandler.java | 4 +- 14 files changed, 148 insertions(+), 386 deletions(-) delete mode 100644 wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java create mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/ParseException.java delete mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java create mode 100644 wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SerializationUtil.java diff --git a/build.gradle.kts b/build.gradle.kts index 299a7f1..0ae267a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,7 +29,6 @@ subprojects { dependencies { implementation(Annotations.id) - implementation(Gson.id) implementation(Log4j2.api) runtimeOnly(Log4j2.core) diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index ce58ce4..20f403d 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -33,11 +33,11 @@ object Mockito { const val jupiter = "$group:mockito-junit-jupiter:$version" } -object Gson { - const val version = "2.8.8" - const val group = "com.google.code.gson" +object Jackson { + const val version = "2.13.1" - const val id = "$group:gson:$version" + const val databind = "com.fasterxml.jackson.core:jackson-databind:$version" + const val module_parameter_names = "com.fasterxml.jackson.module:jackson-module-parameter-names:$version" } object Annotations { diff --git a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java index 3446072..4ca3928 100644 --- a/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java +++ b/wizard-client/wizard-client-cli/src/main/java/eu/jonahbauer/wizard/client/cli/state/Game.java @@ -76,7 +76,7 @@ public final class Game extends BaseState { } } } else if (observerMessage instanceof HandMessage hand) { - hands.put(hand.getPlayer(), hand.getHand()); + hands.put(hand.getPlayer(), new ArrayList<>(hand.getHand())); if (hand.getPlayer().equals(self)) { client.printfln("Your hand cards are: %s", hand.getHand()); } else { diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java index 8e70bc4..b311cee 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java @@ -172,7 +172,7 @@ public final class Game extends BaseState { if (juggling) checkActivePlayer(player, JUGGLE_CARD); finishInteraction(); - hands.put(player, hand); + hands.put(player, new ArrayList<>(hand)); gameScreen.setSelectedCard(null); gameScreen.setHand(player, hand, juggling); juggling = false; diff --git a/wizard-common/build.gradle.kts b/wizard-common/build.gradle.kts index e69de29..9d6a127 100644 --- a/wizard-common/build.gradle.kts +++ b/wizard-common/build.gradle.kts @@ -0,0 +1,10 @@ +val implementation by configurations + +dependencies { + implementation(Jackson.databind) + implementation(Jackson.module_parameter_names) +} + +tasks.withType { + options.compilerArgs.add("-parameters") +} \ No newline at end of file diff --git a/wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java b/wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java deleted file mode 100644 index 78cb7fe..0000000 --- a/wizard-common/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.gson.typeadapters; - -import com.google.gson.*; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
- *   }
- * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. - * - *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
- * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
- *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
- * }
- * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
- * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
- * }
- * - *

Serialization and deserialization

- * In order to serialize and deserialize a polymorphic object, - * you must specify the base type explicitly. - *
   {@code
- *   Diamond diamond = new Diamond();
- *   String json = gson.toJson(diamond, Shape.class);
- * }
- * And then: - *
   {@code
- *   Shape shape = gson.fromJson(json, Shape.class);
- * }
- */ -@SuppressWarnings("ALL") -public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap<>(); - private final Map, String> subtypeToLabel = new LinkedHashMap<>(); - private final boolean maintainType; - - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - this.maintainType = maintainType; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { - return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType); - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory<>(baseType, "type", false); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; - } - - final TypeAdapter jsonElementAdapter = gson.getAdapter(JsonElement.class); - final Map> labelToDelegate - = new LinkedHashMap<>(); - final Map, TypeAdapter> subtypeToDelegate - = new LinkedHashMap<>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = jsonElementAdapter.read(in); - JsonElement labelJsonElement; - if (maintainType) { - labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - } else { - labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - } - - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - throw new JsonParseException("cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype?"); - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - - if (maintainType) { - jsonElementAdapter.write(out, jsonObject); - return; - } - - JsonObject clone = new JsonObject(); - - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - clone.add(typeFieldName, new JsonPrimitive(label)); - - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - jsonElementAdapter.write(out, clone); - } - }.nullSafe(); - } -} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/ParseException.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/ParseException.java new file mode 100644 index 0000000..9bb778e --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/ParseException.java @@ -0,0 +1,7 @@ +package eu.jonahbauer.wizard.common.messages; + +public class ParseException extends RuntimeException { + public ParseException(Throwable cause) { + super(cause); + } +} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/client/ClientMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/client/ClientMessage.java index 54d0833..34d64e9 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/client/ClientMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/client/ClientMessage.java @@ -1,25 +1,23 @@ package eu.jonahbauer.wizard.common.messages.client; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.jonahbauer.wizard.common.messages.ParseException; import eu.jonahbauer.wizard.common.messages.player.PlayerMessage; -import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import eu.jonahbauer.wizard.common.util.SerializationUtil; import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; @EqualsAndHashCode public abstract sealed class ClientMessage permits CreateSessionMessage, InteractionMessage, JoinSessionMessage, LeaveSessionMessage, ReadyMessage, RejoinMessage { - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ClientMessage.class, "Message")) - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message")) - .create(); + private static final ObjectMapper MAPPER = SerializationUtil.newObjectMapper(ClientMessage.class, PlayerMessage.class); - public static ClientMessage parse(String json) throws JsonParseException { - return GSON.fromJson(json, ClientMessage.class); + public static ClientMessage parse(String json) throws ParseException { + return SerializationUtil.parse(json, MAPPER, ClientMessage.class); } @Override + @SneakyThrows public String toString() { - return GSON.toJson(this, ClientMessage.class); + return MAPPER.writeValueAsString(this); } } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java index ef32b07..e667640 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/observer/ObserverMessage.java @@ -1,23 +1,22 @@ package eu.jonahbauer.wizard.common.messages.observer; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; -import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.jonahbauer.wizard.common.messages.ParseException; +import eu.jonahbauer.wizard.common.util.SerializationUtil; import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; @EqualsAndHashCode public abstract sealed class ObserverMessage permits CardMessage, HandMessage, PredictionMessage, ScoreMessage, StateMessage, TimeoutMessage, TrickMessage, TrumpMessage, UserInputMessage { - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message")) - .create(); + private static final ObjectMapper MAPPER = SerializationUtil.newObjectMapper(ObserverMessage.class); - public static ObserverMessage parse(String json) throws JsonParseException { - return GSON.fromJson(json, ObserverMessage.class); + public static ObserverMessage parse(String json) throws ParseException { + return SerializationUtil.parse(json, MAPPER, ObserverMessage.class); } @Override + @SneakyThrows public String toString() { - return GSON.toJson(this, ObserverMessage.class); + return MAPPER.writeValueAsString(this); } } \ No newline at end of file diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java index 180c7f1..f906231 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/player/PlayerMessage.java @@ -1,23 +1,22 @@ package eu.jonahbauer.wizard.common.messages.player; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; -import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.jonahbauer.wizard.common.messages.ParseException; +import eu.jonahbauer.wizard.common.util.SerializationUtil; import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; @EqualsAndHashCode public abstract sealed class PlayerMessage permits ContinueMessage, JuggleMessage, PickTrumpMessage, PlayCardMessage, PredictMessage { - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message")) - .create(); + private static final ObjectMapper MAPPER = SerializationUtil.newObjectMapper(PlayerMessage.class); - public static PlayerMessage parse(String json) throws JsonParseException { - return GSON.fromJson(json, PlayerMessage.class); + public static PlayerMessage parse(String json) throws ParseException { + return SerializationUtil.parse(json, MAPPER, PlayerMessage.class); } @Override + @SneakyThrows public String toString() { - return GSON.toJson(this); + return MAPPER.writeValueAsString(this); } } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/server/ServerMessage.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/server/ServerMessage.java index 54846e4..448534d 100644 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/server/ServerMessage.java +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/messages/server/ServerMessage.java @@ -1,25 +1,23 @@ package eu.jonahbauer.wizard.common.messages.server; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; +import com.fasterxml.jackson.databind.ObjectMapper; +import eu.jonahbauer.wizard.common.messages.ParseException; import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; -import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory; +import eu.jonahbauer.wizard.common.util.SerializationUtil; import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; @EqualsAndHashCode public abstract sealed class ServerMessage permits AckMessage, GameMessage, KickVotedMessage, KickedMessage, NackMessage, PlayerLeftMessage, PlayerModifiedMessage, SessionJoinedMessage, SessionListMessage, SessionModifiedMessage, SessionRemovedMessage, StartingGameMessage { - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ServerMessage.class, "Message")) - .registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message")) - .create(); + private static final ObjectMapper MAPPER = SerializationUtil.newObjectMapper(ServerMessage.class, ObserverMessage.class); - public static ServerMessage parse(String json) throws JsonParseException { - return GSON.fromJson(json, ServerMessage.class); + public static ServerMessage parse(String json) throws ParseException { + return SerializationUtil.parse(json, MAPPER, ServerMessage.class); } @Override + @SneakyThrows public String toString() { - return GSON.toJson(this, ServerMessage.class); + return MAPPER.writeValueAsString(this); } } diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java deleted file mode 100644 index 2e07ea5..0000000 --- a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SealedClassTypeAdapterFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package eu.jonahbauer.wizard.common.util; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Modifier; -import java.util.Locale; - -public final class SealedClassTypeAdapterFactory implements TypeAdapterFactory { - private final RuntimeTypeAdapterFactory factory; - - public static SealedClassTypeAdapterFactory of(Class clazz) { - return new SealedClassTypeAdapterFactory<>(clazz, null); - } - - public static SealedClassTypeAdapterFactory of(Class clazz, @Nullable String suffix) { - return new SealedClassTypeAdapterFactory<>(clazz, suffix); - } - - private SealedClassTypeAdapterFactory(Class clazz, @Nullable String suffix) { - factory = RuntimeTypeAdapterFactory.of(clazz); - register(clazz, suffix); - } - - private void register(Class clazz, String suffix) { - for (Class subclass : clazz.getPermittedSubclasses()) { - int modifiers = subclass.getModifiers(); - if (Modifier.isFinal(modifiers) || subclass.isSealed() && !Modifier.isAbstract(modifiers)) { - String name = subclass.getSimpleName(); - - // remove suffix - if (suffix != null) { - if (name.endsWith(suffix)) name = name.substring(0, name.length() - suffix.length()); - } - - // transform camelCast to snake_case - name = name.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT); - - factory.registerSubtype(subclass.asSubclass(clazz), name); - } - if (subclass.isSealed()) { - register(subclass.asSubclass(clazz), suffix); - } else if (!Modifier.isFinal(modifiers)) { - //subclass is non-sealed - throw new IllegalArgumentException( - "SealedClassTypeAdapterFactory does not support a type hierarchy that contains non-sealed classes. " + - "Found non-sealed class " + subclass.getName() - ); - } - } - - } - - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - return factory.create(gson, type); - } -} diff --git a/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SerializationUtil.java b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SerializationUtil.java new file mode 100644 index 0000000..c8babeb --- /dev/null +++ b/wizard-common/src/main/java/eu/jonahbauer/wizard/common/util/SerializationUtil.java @@ -0,0 +1,87 @@ +package eu.jonahbauer.wizard.common.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.*; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import eu.jonahbauer.wizard.common.messages.ParseException; +import lombok.experimental.UtilityClass; + +import java.lang.reflect.Modifier; +import java.util.Locale; + +@UtilityClass +public class SerializationUtil { + public static ObjectMapper newObjectMapper(Class...classes) { + var mapper = new ObjectMapper() + .registerModule(new ParameterNamesModule(JsonCreator.Mode.PROPERTIES)) + .registerModule(new SimpleModule() { + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + for (Class clazz : classes) { + context.setMixInAnnotations(clazz, Mixin.class); + } + context.insertAnnotationIntrospector(new NopAnnotationIntrospector() { + @Override + public JsonCreator.Mode findCreatorAnnotation(MapperConfig config, Annotated ann) { + if (ann instanceof AnnotatedConstructor con) { + var declaringClass = con.getDeclaringClass(); + for (Class clazz : classes) { + if (clazz.isAssignableFrom(declaringClass)) { + return JsonCreator.Mode.PROPERTIES; + } + } + } + return super.findCreatorAnnotation(config, ann); + } + }); + } + }); + + for (Class clazz : classes) { + registerSubtypes(mapper, clazz); + } + + return mapper; + } + + private static void registerSubtypes(ObjectMapper objectMapper, Class clazz) { + if (!clazz.isSealed()) throw new IllegalArgumentException(); + + var suffix = "Message"; + for (Class subclass : clazz.getPermittedSubclasses()) { + int modifiers = subclass.getModifiers(); + + if (Modifier.isFinal(modifiers) || subclass.isSealed() && !Modifier.isAbstract(modifiers)) { + var name = subclass.getSimpleName(); + if (name.endsWith(suffix)) name = name.substring(0, name.length() - suffix.length()); + name = name.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(Locale.ROOT); + + objectMapper.registerSubtypes(new NamedType(subclass, name)); + } + + if (subclass.isSealed()) { + registerSubtypes(objectMapper, subclass); + } else if (!Modifier.isFinal(modifiers)) { + throw new IllegalArgumentException("Non-sealed classes are not supported."); + } + } + } + + public static T parse(String json, ObjectMapper objectMapper, Class clazz) throws ParseException { + try { + return objectMapper.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new ParseException(e); + } + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + private static class Mixin {} +} diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java index 7b54f22..b8aaa51 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/socket/WizardSocketHandler.java @@ -1,6 +1,6 @@ package eu.jonahbauer.wizard.server.socket; -import com.google.gson.JsonParseException; +import eu.jonahbauer.wizard.common.messages.ParseException; import eu.jonahbauer.wizard.common.messages.client.ClientMessage; import eu.jonahbauer.wizard.common.messages.server.NackMessage; import eu.jonahbauer.wizard.server.NackException; @@ -37,7 +37,7 @@ public class WizardSocketHandler extends TextWebSocketHandler { player.onMessage(ClientMessage.parse(text.getPayload())); } catch (NackException e) { player.send(e.toMessage()); - } catch (JsonParseException e) { + } catch (ParseException e) { player.send(new NackMessage(NackMessage.MALFORMED_MESSAGE, "Could not parse " + text.getPayload() + ".")); } }