migrated to jackson

This commit is contained in:
2022-01-12 10:28:31 +01:00
parent b20f300260
commit 94ac10e93b
14 changed files with 148 additions and 386 deletions

View File

@@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.common.messages;
public class ParseException extends RuntimeException {
public ParseException(Throwable cause) {
super(cause);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<T> implements TypeAdapterFactory {
private final RuntimeTypeAdapterFactory<T> factory;
public static <T> SealedClassTypeAdapterFactory<T> of(Class<T> clazz) {
return new SealedClassTypeAdapterFactory<>(clazz, null);
}
public static <T> SealedClassTypeAdapterFactory<T> of(Class<T> clazz, @Nullable String suffix) {
return new SealedClassTypeAdapterFactory<>(clazz, suffix);
}
private SealedClassTypeAdapterFactory(Class<T> clazz, @Nullable String suffix) {
factory = RuntimeTypeAdapterFactory.of(clazz);
register(clazz, suffix);
}
private void register(Class<? extends T> 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 <S> TypeAdapter<S> create(Gson gson, TypeToken<S> type) {
return factory.create(gson, type);
}
}

View File

@@ -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> T parse(String json, ObjectMapper objectMapper, Class<T> 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 {}
}