From 3ea01199db7f958f08e40704810ce2931ade84c6 Mon Sep 17 00:00:00 2001 From: Exlll Date: Sat, 9 Jul 2022 02:06:04 +0200 Subject: [PATCH] Add serialization support for Bukkit's ConfigurationSerializable types --- .../configlib/ConfigurationProperties.java | 48 +++++++++++++++++-- .../exlll/configlib/SerializerSelector.java | 17 ++++++- .../ConfigurationPropertiesTest.java | 36 ++++++++++++-- .../configlib/SerializerSelectorTest.java | 39 ++++++++++++++- .../java/de/exlll/configlib/TestUtils.java | 22 +++++++++ ...itConfigurationSerializableSerializer.java | 31 ++++++++++++ .../java/de/exlll/configlib/ConfigLib.java | 27 ++++++++++- 7 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 configlib-paper/src/main/java/de/exlll/configlib/BukkitConfigurationSerializableSerializer.java diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java index 13e2d07..35aa374 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java @@ -1,7 +1,8 @@ package de.exlll.configlib; -import java.util.HashMap; -import java.util.Map; +import java.lang.reflect.Type; +import java.util.*; +import java.util.function.Predicate; import static de.exlll.configlib.Validator.requireNonNull; @@ -10,6 +11,7 @@ import static de.exlll.configlib.Validator.requireNonNull; */ class ConfigurationProperties { private final Map, Serializer> serializersByType; + private final Map, Serializer> serializersByCondition; private final FieldFormatter formatter; private final FieldFilter filter; private final boolean outputNulls; @@ -24,6 +26,9 @@ class ConfigurationProperties { */ protected ConfigurationProperties(Builder builder) { this.serializersByType = Map.copyOf(builder.serializersByType); + this.serializersByCondition = Collections.unmodifiableMap(new LinkedHashMap<>( + builder.serializersByCondition + )); this.formatter = requireNonNull(builder.formatter, "field formatter"); this.filter = requireNonNull(builder.filter, "field filter"); this.outputNulls = builder.outputNulls; @@ -68,7 +73,8 @@ class ConfigurationProperties { */ public static abstract class Builder> { private final Map, Serializer> serializersByType = new HashMap<>(); - /* change setter JavaDoc if default values are changed */ + private final Map, Serializer> serializersByCondition = + new LinkedHashMap<>(); private FieldFormatter formatter = FieldFormatters.IDENTITY; private FieldFilter filter = FieldFilters.DEFAULT; private boolean outputNulls = false; @@ -79,6 +85,7 @@ class ConfigurationProperties { protected Builder(ConfigurationProperties properties) { this.serializersByType.putAll(properties.serializersByType); + this.serializersByCondition.putAll(properties.serializersByCondition); this.formatter = properties.formatter; this.filter = properties.filter; this.outputNulls = properties.outputNulls; @@ -131,6 +138,29 @@ class ConfigurationProperties { return getThis(); } + /** + * Adds a serializer for the condition. The serializer is selected when the condition + * evaluates to true. The {@code test} method of the condition object is invoked with + * the type of a field. Serializers added by this method take precedence over all other + * serializers expect the ones that were added for a specific type by the + * {@link #addSerializer(Class, Serializer)} method. The conditions are checked in the order + * in which they were added. + * + * @param condition the condition + * @param serializer the serializer + * @return this builder + * @throws NullPointerException if any argument is null + */ + final B addSerializerByCondition( + Predicate condition, + Serializer serializer + ) { + requireNonNull(condition, "condition"); + requireNonNull(serializer, "serializer"); + serializersByCondition.put(condition, serializer); + return getThis(); + } + /** * Sets whether fields or collection elements whose value is null should be output * while serializing the configuration. @@ -215,6 +245,18 @@ class ConfigurationProperties { return serializersByType; } + /** + * Returns an unmodifiable map of serializers by condition. The serializers returned by this + * method take precedence over any default serializers provided by this library expect the ones + * that were added for a specific type. + * + * @return serializers by condition + */ + final Map, Serializer> getSerializersByCondition() { + return serializersByCondition; + } + + /** * Returns whether null values should be output. * diff --git a/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java b/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java index 9d2b4b6..edfeb93 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java +++ b/configlib-core/src/main/java/de/exlll/configlib/SerializerSelector.java @@ -42,6 +42,9 @@ final class SerializerSelector { } public Serializer select(Type type) { + final Serializer custom = selectCustomSerializer(type); + if (custom != null) + return custom; if (type instanceof Class cls) { return selectForClass(cls); } else if (type instanceof ParameterizedType pType) { @@ -60,9 +63,19 @@ final class SerializerSelector { throw new ConfigurationException(baseExceptionMessage(type)); } + private Serializer selectCustomSerializer(Type type) { + if (type instanceof Class cls) { + if (properties.getSerializers().containsKey(cls)) // TODO move check out + return properties.getSerializers().get(cls); + } + for (var entry : properties.getSerializersByCondition().entrySet()) { + if (entry.getKey().test(type)) + return entry.getValue(); + } + return null; + } + private Serializer selectForClass(Class cls) { - if (properties.getSerializers().containsKey(cls)) - return properties.getSerializers().get(cls); if (DEFAULT_SERIALIZERS.containsKey(cls)) return DEFAULT_SERIALIZERS.get(cls); if (Reflect.isEnumType(cls)) { diff --git a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java index c6f57ce..3ae497b 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/ConfigurationPropertiesTest.java @@ -1,10 +1,13 @@ package de.exlll.configlib; +import de.exlll.configlib.Serializers.StringSerializer; import org.junit.jupiter.api.Test; import java.awt.Point; +import java.lang.reflect.Type; import java.util.Locale; import java.util.Map; +import java.util.function.Predicate; import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException; import static org.hamcrest.MatcherAssert.assertThat; @@ -21,6 +24,7 @@ class ConfigurationPropertiesTest { assertThat(properties.outputNulls(), is(false)); assertThat(properties.inputNulls(), is(false)); assertThat(properties.getSerializers().entrySet(), empty()); + assertThat(properties.getSerializersByCondition().entrySet(), empty()); assertThat(properties.getFieldFormatter(), is(FieldFormatters.IDENTITY)); assertThat(properties.getFieldFilter(), is(FieldFilters.DEFAULT)); } @@ -30,9 +34,11 @@ class ConfigurationPropertiesTest { FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT); FieldFilter filter = field -> field.getName().startsWith("f"); TestUtils.PointSerializer serializer = new TestUtils.PointSerializer(); + Predicate predicate = type -> true; ConfigurationProperties properties = ConfigurationProperties.newBuilder() .addSerializer(Point.class, serializer) + .addSerializerByCondition(predicate, serializer) .setFieldFormatter(formatter) .setFieldFilter(filter) .outputNulls(true) @@ -41,6 +47,7 @@ class ConfigurationPropertiesTest { .build(); assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer))); + assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer))); assertThat(properties.outputNulls(), is(true)); assertThat(properties.inputNulls(), is(true)); assertThat(properties.serializeSetsAsLists(), is(false)); @@ -53,9 +60,11 @@ class ConfigurationPropertiesTest { FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT); FieldFilter filter = field -> field.getName().startsWith("f"); TestUtils.PointSerializer serializer = new TestUtils.PointSerializer(); + Predicate predicate = type -> true; ConfigurationProperties properties = ConfigurationProperties.newBuilder() .addSerializer(Point.class, serializer) + .addSerializerByCondition(predicate, serializer) .setFieldFormatter(formatter) .setFieldFilter(filter) .outputNulls(true) @@ -66,6 +75,7 @@ class ConfigurationPropertiesTest { .build(); assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer))); + assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer))); assertThat(properties.outputNulls(), is(true)); assertThat(properties.inputNulls(), is(true)); assertThat(properties.serializeSetsAsLists(), is(false)); @@ -76,11 +86,16 @@ class ConfigurationPropertiesTest { @Test void builderSerializersUnmodifiable() { ConfigurationProperties properties = ConfigurationProperties.newBuilder().build(); - Map, Serializer> map = properties.getSerializers(); + var serializersByType = properties.getSerializers(); + var serializersByCondition = properties.getSerializersByCondition(); assertThrows( UnsupportedOperationException.class, - () -> map.put(Point.class, new TestUtils.PointSerializer()) + () -> serializersByType.put(Point.class, new TestUtils.PointSerializer()) + ); + assertThrows( + UnsupportedOperationException.class, + () -> serializersByCondition.put(t -> true, new TestUtils.PointSerializer()) ); } @@ -104,9 +119,9 @@ class ConfigurationPropertiesTest { } @Test - void addSerializerRequiresNonNull() { + void addSerializerByTypeRequiresNonNull() { assertThrowsNullPointerException( - () -> builder.addSerializer(null, new Serializers.StringSerializer()), + () -> builder.addSerializer(null, new StringSerializer()), "serialized type" ); @@ -115,5 +130,18 @@ class ConfigurationPropertiesTest { "serializer" ); } + + @Test + void addSerializerByConditionRequiresNonNull() { + assertThrowsNullPointerException( + () -> builder.addSerializerByCondition(null, new StringSerializer()), + "condition" + ); + + assertThrowsNullPointerException( + () -> builder.addSerializerByCondition(type -> true, null), + "serializer" + ); + } } } \ No newline at end of file diff --git a/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java b/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java index 9b30956..3da377d 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java +++ b/configlib-core/src/test/java/de/exlll/configlib/SerializerSelectorTest.java @@ -178,7 +178,7 @@ class SerializerSelectorTest { } @Test - void selectSerializerCustomType() { + void selectSerializerByCustomType() { var properties = ConfigurationProperties.newBuilder() .addSerializer(Point.class, POINT_SERIALIZER) .build(); @@ -188,7 +188,7 @@ class SerializerSelectorTest { } @Test - void selectSerializerCustomSerializerTakesPrecedence() { + void selectSerializerByCustomTypeTakesPrecedence() { var properties = ConfigurationProperties.newBuilder() .addSerializer(BigInteger.class, CUSTOM_BIG_INTEGER_SERIALIZER) .build(); @@ -198,6 +198,41 @@ class SerializerSelectorTest { assertThat(bigIntegerSerializer, sameInstance(CUSTOM_BIG_INTEGER_SERIALIZER)); } + @Test + void selectSerializerByCondition() { + var properties = ConfigurationProperties.newBuilder() + .addSerializerByCondition(t -> t == Point.class, POINT_SERIALIZER) + .build(); + SerializerSelector selector = new SerializerSelector(properties); + var pointSerializer = selector.select(Point.class); + assertThat(pointSerializer, sameInstance(POINT_SERIALIZER)); + } + + @Test + void selectSerializerByConditionTakesPrecedence() { + var properties = ConfigurationProperties.newBuilder() + .addSerializerByCondition(t -> t == BigInteger.class, CUSTOM_BIG_INTEGER_SERIALIZER) + .build(); + SerializerSelector selector = new SerializerSelector(properties); + var bigIntegerSerializer = selector.select(BigInteger.class); + assertThat(bigIntegerSerializer, instanceOf(TestUtils.CustomBigIntegerSerializer.class)); + assertThat(bigIntegerSerializer, sameInstance(CUSTOM_BIG_INTEGER_SERIALIZER)); + } + + @Test + void selectSerializerByCustomTypeTakesPrecedenceOverCustomType() { + var serializer1 = IdentifiableSerializer.of(1); + var serializer2 = IdentifiableSerializer.of(2); + var properties = ConfigurationProperties.newBuilder() + .addSerializerByCondition(t -> t == int.class, serializer1) + .addSerializer(int.class, serializer2) + .build(); + SerializerSelector selector = new SerializerSelector(properties); + var serializer = selector.select(int.class); + assertThat(serializer, instanceOf(IdentifiableSerializer.class)); + assertThat(serializer, sameInstance(serializer2)); + } + @Test void selectSerializerList() { class A { diff --git a/configlib-core/src/test/java/de/exlll/configlib/TestUtils.java b/configlib-core/src/test/java/de/exlll/configlib/TestUtils.java index cbcfe27..d180978 100644 --- a/configlib-core/src/test/java/de/exlll/configlib/TestUtils.java +++ b/configlib-core/src/test/java/de/exlll/configlib/TestUtils.java @@ -94,6 +94,28 @@ public final class TestUtils { } } + public static final class IdentifiableSerializer implements Serializer { + public int identifier; + + public IdentifiableSerializer(int identifier) { + this.identifier = identifier; + } + + public static IdentifiableSerializer of(int identifier) { + return new IdentifiableSerializer<>(identifier); + } + + @Override + public T serialize(S element) { + return null; + } + + @Override + public S deserialize(T element) { + return null; + } + } + public static final class PointIdentitySerializer implements Serializer { @Override public Point serialize(Point element) { diff --git a/configlib-paper/src/main/java/de/exlll/configlib/BukkitConfigurationSerializableSerializer.java b/configlib-paper/src/main/java/de/exlll/configlib/BukkitConfigurationSerializableSerializer.java new file mode 100644 index 0000000..c2eb9e8 --- /dev/null +++ b/configlib-paper/src/main/java/de/exlll/configlib/BukkitConfigurationSerializableSerializer.java @@ -0,0 +1,31 @@ +package de.exlll.configlib; + +import org.bukkit.configuration.file.YamlConstructor; +import org.bukkit.configuration.file.YamlRepresenter; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +final class BukkitConfigurationSerializableSerializer + implements Serializer { + static BukkitConfigurationSerializableSerializer DEFAULT = + new BukkitConfigurationSerializableSerializer(); + private final Yaml yaml; + + BukkitConfigurationSerializableSerializer() { + DumperOptions options = new DumperOptions(); + options.setIndent(2); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + this.yaml = new Yaml(new YamlConstructor(), new YamlRepresenter(), options); + } + + @Override + public String serialize(ConfigurationSerializable element) { + return yaml.dump(element); + } + + @Override + public ConfigurationSerializable deserialize(String element) { + return yaml.load(element); + } +} \ No newline at end of file diff --git a/configlib-paper/src/main/java/de/exlll/configlib/ConfigLib.java b/configlib-paper/src/main/java/de/exlll/configlib/ConfigLib.java index 3ebabc4..41d110e 100644 --- a/configlib-paper/src/main/java/de/exlll/configlib/ConfigLib.java +++ b/configlib-paper/src/main/java/de/exlll/configlib/ConfigLib.java @@ -1,8 +1,31 @@ package de.exlll.configlib; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; /** - * An empty plugin class that loads this library and its dependencies. + * The plugin class that loads this library and its dependencies. */ -public final class ConfigLib extends JavaPlugin {} +public final class ConfigLib extends JavaPlugin { + /** + * A {@code YamlConfigurationProperties} object that provides serializers for several Bukkit + * classes like {@link ItemStack} and other {@link ConfigurationSerializable} types. + *

+ * You can configure these properties further by creating a new builder using the + * {@code toBuilder()} method of this object. + */ + public static final YamlConfigurationProperties BUKKIT_DEFAULT_PROPERTIES = + initializeBukkitDefaultProperties(); + + private static YamlConfigurationProperties initializeBukkitDefaultProperties() { + return YamlConfigurationProperties + .newBuilder() + .addSerializerByCondition( + type -> type instanceof Class cls && + ConfigurationSerializable.class.isAssignableFrom(cls), + BukkitConfigurationSerializableSerializer.DEFAULT + ) + .build(); + } +}