- Jubiläumsedition implementiert (#12)

- Tests verbessert
This commit is contained in:
2021-11-04 20:18:26 +01:00
parent 1775604e1d
commit 66f6d10330
71 changed files with 2472 additions and 812 deletions

View File

@@ -0,0 +1,273 @@
/*
* 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:
* <pre> {@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;
* }
* }</pre>
* <p>Without additional type information, the serialized JSON is ambiguous. Is
* the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
* {
* "bottomShape": {
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* This class addresses this problem by adding type information to the
* serialized JSON and honoring that type information when the JSON is
* deserialized: <pre> {@code
* {
* "bottomShape": {
* "type": "Diamond",
* "width": 10,
* "height": 5,
* "x": 0,
* "y": 0
* },
* "topShape": {
* "type": "Circle",
* "radius": 2,
* "x": 4,
* "y": 1
* }
* }}</pre>
* Both the type field name ({@code "type"}) and the type labels ({@code
* "Rectangle"}) are configurable.
*
* <h3>Registering Types</h3>
* 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. <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
* = RuntimeTypeAdapterFactory.of(Shape.class, "type");
* }</pre>
* 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.
* <pre> {@code
* shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
* shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
* shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
* }</pre>
* Finally, register the type adapter factory in your application's GSON builder:
* <pre> {@code
* Gson gson = new GsonBuilder()
* .registerTypeAdapterFactory(shapeAdapterFactory)
* .create();
* }</pre>
* Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
* RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
* .registerSubtype(Rectangle.class)
* .registerSubtype(Circle.class)
* .registerSubtype(Diamond.class);
* }</pre>
*
* <h3>Serialization and deserialization</h3>
* In order to serialize and deserialize a polymorphic object,
* you must specify the base type explicitly.
* <pre> {@code
* Diamond diamond = new Diamond();
* String json = gson.toJson(diamond, Shape.class);
* }</pre>
* And then:
* <pre> {@code
* Shape shape = gson.fromJson(json, Shape.class);
* }</pre>
*/
@SuppressWarnings("ALL")
public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
private final Class<?> baseType;
private final String typeFieldName;
private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
private final Map<Class<?>, 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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> 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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> 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 <T> RuntimeTypeAdapterFactory<T> of(Class<T> 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<T> registerSubtype(Class<? extends T> 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<T> registerSubtype(Class<? extends T> type) {
return registerSubtype(type, type.getSimpleName());
}
@Override
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (type.getRawType() != baseType) {
return null;
}
final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
final Map<String, TypeAdapter<?>> labelToDelegate
= new LinkedHashMap<>();
final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
= new LinkedHashMap<>();
for (Map.Entry<String, Class<?>> 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<R>() {
@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<R> delegate = (TypeAdapter<R>) 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<R> delegate = (TypeAdapter<R>) 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<String, JsonElement> e : jsonObject.entrySet()) {
clone.add(e.getKey(), e.getValue());
}
jsonElementAdapter.write(out, clone);
}
}.nullSafe();
}
}

View File

@@ -0,0 +1,26 @@
package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* A {@link CardMessage} is sent whenever a player plays a card.
*/
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class CardMessage extends ObserverMessage {
/**
* The UUID of the player.
*/
private final @NotNull UUID player;
/**
* The card played.
*/
private final @NotNull Card card;
}

View File

@@ -0,0 +1,31 @@
package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.UUID;
/**
* A {@link HandMessage} is sent when the player receives information about hit own or another player's hand cards.
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public final class HandMessage extends ObserverMessage {
/**
* The UUID of player whose hand cards are sent.
*/
private final @NotNull UUID player;
/**
* A list of all the hand cards. May consist only of {@link Card#HIDDEN} if the cars are not visible to the player
* receiving this message
*/
private final @NotNull List<@NotNull Card> hand;
public HandMessage(@NotNull UUID player, @NotNull List<@NotNull Card> hand) {
this.player = player;
this.hand = List.copyOf(hand);
}
}

View File

@@ -0,0 +1,7 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public final class JugglingMessage extends ObserverMessage {
}

View File

@@ -0,0 +1,18 @@
package eu.jonahbauer.wizard.common.messages.observer;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public abstract sealed class ObserverMessage permits CardMessage, HandMessage, JugglingMessage, PredictionMessage, ScoreMessage, StateMessage, TrickMessage, TrumpMessage, UserInputMessage {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(ObserverMessage.class, "Message"))
.create();
@Override
public String toString() {
return GSON.toJson(this, ObserverMessage.class);
}
}

View File

@@ -0,0 +1,26 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Range;
import java.util.UUID;
/**
* A {@link PredictionMessage} is sent after a player makes (or changes) his prediction.
*/
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PredictionMessage extends ObserverMessage {
/**
* The UUID of the player who made a prediction.
*/
private final @NotNull UUID player;
/**
* The prediction.
*/
private final @Range(from = 0, to = Integer.MAX_VALUE) int prediction;
}

View File

@@ -0,0 +1,25 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* A {@link ScoreMessage} is sent at the end of each round and at the end of the game. It contains the number of points
* gained by each player in this round or the final result.
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public final class ScoreMessage extends ObserverMessage {
/**
* The number of points for each player.
*/
private final @NotNull Map<@NotNull UUID, @NotNull Integer> points;
public ScoreMessage(@NotNull Map<@NotNull UUID, @NotNull Integer> points) {
this.points = Map.copyOf(points);
}
}

View File

@@ -0,0 +1,18 @@
package eu.jonahbauer.wizard.common.messages.observer;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* A {@link StateMessage} is sent whenever the game changes its internal state.
*/
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class StateMessage extends ObserverMessage {
/**
* The name of the new state in snake_case.
*/
private final String state;
}

View File

@@ -0,0 +1,30 @@
package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.List;
import java.util.UUID;
/**
* A {@link TrickMessage} is sent when a trick is completed. It contains the player who won the trick and a list of the
* cards played.
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public final class TrickMessage extends ObserverMessage {
/**
* The UUID of the player who won the trick.
*/
private final UUID player;
/**
* The cards played.
*/
private final List<Card> cards;
public TrickMessage(UUID player, List<Card> cards) {
this.player = player;
this.cards = List.copyOf(cards);
}
}

View File

@@ -0,0 +1,30 @@
package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.jetbrains.annotations.Nullable;
/**
* A {@link TrumpMessage} is sent when the trump suit of the current round is (being) determined.
*/
@Getter
@EqualsAndHashCode(callSuper = true)
public final class TrumpMessage extends ObserverMessage {
/**
* The {@link Card} that was revealed to determine the {@linkplain Card.Suit trump suit} or {@code null} no cards
* were left.
*/
private final @Nullable Card card;
/**
* The trump suit or {@code null} is the dealer has yet to decide.
*/
private final @Nullable Card.Suit suit;
public TrumpMessage(@Nullable Card card, @Nullable Card.Suit suit) {
if (card == null && suit == null) throw new IllegalArgumentException("Card and suit must not both be null");
this.card = card;
this.suit = suit;
}
}

View File

@@ -0,0 +1,54 @@
package eu.jonahbauer.wizard.common.messages.observer;
import eu.jonahbauer.wizard.common.messages.player.PickTrumpMessage;
import eu.jonahbauer.wizard.common.messages.player.PlayCardMessage;
import eu.jonahbauer.wizard.common.messages.player.PredictMessage;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.UUID;
/**
* A {@link UserInputMessage} is sent when user input is required.
*/
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class UserInputMessage extends ObserverMessage {
/**
* The UUID of the player whose input is required.
*/
private final UUID player;
/**
* The type of input that is required.
*/
private final Action action;
/**
* A timeout in {@link System#currentTimeMillis() UNIX time} after which a default action is taken.
*/
private final long timeout;
public enum Action {
/**
* An action that indicates that a player should make a prediction. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}.
*/
MAKE_PREDICTION,
/**
* An action that indicates that a player should change his a prediction by ±1. A {@link UserInputMessage} with
* this {@link UserInputMessage#getAction()} should be responded to with a {@link PredictMessage}.
*/
CHANGE_PREDICTION,
/**
* An action that indicates that a player should play a card. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PlayCardMessage}.
*/
PLAY_CARD,
/**
* An action that indicates that a player should pick a trump suit. A {@link UserInputMessage} with this
* {@link UserInputMessage#getAction()} should be responded to with a {@link PickTrumpMessage}.
*/
PICK_TRUMP
}
}

View File

@@ -0,0 +1,13 @@
package eu.jonahbauer.wizard.common.messages.player;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PickTrumpMessage extends PlayerMessage {
private final Card.Suit trumpSuit;
}

View File

@@ -0,0 +1,13 @@
package eu.jonahbauer.wizard.common.messages.player;
import eu.jonahbauer.wizard.common.model.card.Card;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PlayCardMessage extends PlayerMessage {
private final Card card;
}

View File

@@ -0,0 +1,18 @@
package eu.jonahbauer.wizard.common.messages.player;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import eu.jonahbauer.wizard.common.util.SealedClassTypeAdapterFactory;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public abstract sealed class PlayerMessage permits PickTrumpMessage, PlayCardMessage, PredictMessage {
public static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(SealedClassTypeAdapterFactory.of(PlayerMessage.class, "Message"))
.create();
@Override
public String toString() {
return GSON.toJson(this);
}
}

View File

@@ -0,0 +1,12 @@
package eu.jonahbauer.wizard.common.messages.player;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode(callSuper = true)
public final class PredictMessage extends PlayerMessage {
private final int prediction;
}

View File

@@ -0,0 +1,31 @@
package eu.jonahbauer.wizard.common.model.card;
public enum Card {
HIDDEN,
BLUE_1, RED_1, GREEN_1, YELLOW_1,
BLUE_2, RED_2, GREEN_2, YELLOW_2,
BLUE_3, RED_3, GREEN_3, YELLOW_3,
BLUE_4, RED_4, GREEN_4, YELLOW_4,
BLUE_5, RED_5, GREEN_5, YELLOW_5,
BLUE_6, RED_6, GREEN_6, YELLOW_6,
BLUE_7, RED_7, GREEN_7, YELLOW_7,
BLUE_8, RED_8, GREEN_8, YELLOW_8,
BLUE_9, RED_9, GREEN_9, YELLOW_9,
BLUE_10, RED_10, GREEN_10, YELLOW_10,
BLUE_11, RED_11, GREEN_11, YELLOW_11,
BLUE_12, RED_12, GREEN_12, YELLOW_12,
BLUE_13, RED_13, GREEN_13, YELLOW_13,
BLUE_WIZARD, RED_WIZARD, GREEN_WIZARD, YELLOW_WIZARD,
BLUE_JESTER, RED_JESTER, GREEN_JESTER, YELLOW_JESTER,
CHANGELING, CHANGELING_WIZARD, CHANGELING_JESTER,
BOMB,
WEREWOLF,
DRAGON,
FAIRY,
CLOUD, CLOUD_BLUE, CLOUD_RED, CLOUD_GREEN, CLOUD_YELLOW,
JUGGLER, JUGGLER_BLUE, JUGGLER_RED, JUGGLER_GREEN, JUGGLER_YELLOW;
public enum Suit {
NONE, YELLOW, RED, GREEN, BLUE
}
}

View File

@@ -0,0 +1,44 @@
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.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);
for (Class<?> subclass : clazz.getPermittedSubclasses()) {
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);
}
}
@Override
public <S> TypeAdapter<S> create(Gson gson, TypeToken<S> type) {
return factory.create(gson, type);
}
}