Add serialization support for Bukkit's ConfigurationSerializable types

dev
Exlll 3 years ago
parent f70c44f159
commit 3ea01199db

@ -1,7 +1,8 @@
package de.exlll.configlib; package de.exlll.configlib;
import java.util.HashMap; import java.lang.reflect.Type;
import java.util.Map; import java.util.*;
import java.util.function.Predicate;
import static de.exlll.configlib.Validator.requireNonNull; import static de.exlll.configlib.Validator.requireNonNull;
@ -10,6 +11,7 @@ import static de.exlll.configlib.Validator.requireNonNull;
*/ */
class ConfigurationProperties { class ConfigurationProperties {
private final Map<Class<?>, Serializer<?, ?>> serializersByType; private final Map<Class<?>, Serializer<?, ?>> serializersByType;
private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition;
private final FieldFormatter formatter; private final FieldFormatter formatter;
private final FieldFilter filter; private final FieldFilter filter;
private final boolean outputNulls; private final boolean outputNulls;
@ -24,6 +26,9 @@ class ConfigurationProperties {
*/ */
protected ConfigurationProperties(Builder<?> builder) { protected ConfigurationProperties(Builder<?> builder) {
this.serializersByType = Map.copyOf(builder.serializersByType); this.serializersByType = Map.copyOf(builder.serializersByType);
this.serializersByCondition = Collections.unmodifiableMap(new LinkedHashMap<>(
builder.serializersByCondition
));
this.formatter = requireNonNull(builder.formatter, "field formatter"); this.formatter = requireNonNull(builder.formatter, "field formatter");
this.filter = requireNonNull(builder.filter, "field filter"); this.filter = requireNonNull(builder.filter, "field filter");
this.outputNulls = builder.outputNulls; this.outputNulls = builder.outputNulls;
@ -68,7 +73,8 @@ class ConfigurationProperties {
*/ */
public static abstract class Builder<B extends Builder<B>> { public static abstract class Builder<B extends Builder<B>> {
private final Map<Class<?>, Serializer<?, ?>> serializersByType = new HashMap<>(); private final Map<Class<?>, Serializer<?, ?>> serializersByType = new HashMap<>();
/* change setter JavaDoc if default values are changed */ private final Map<Predicate<? super Type>, Serializer<?, ?>> serializersByCondition =
new LinkedHashMap<>();
private FieldFormatter formatter = FieldFormatters.IDENTITY; private FieldFormatter formatter = FieldFormatters.IDENTITY;
private FieldFilter filter = FieldFilters.DEFAULT; private FieldFilter filter = FieldFilters.DEFAULT;
private boolean outputNulls = false; private boolean outputNulls = false;
@ -79,6 +85,7 @@ class ConfigurationProperties {
protected Builder(ConfigurationProperties properties) { protected Builder(ConfigurationProperties properties) {
this.serializersByType.putAll(properties.serializersByType); this.serializersByType.putAll(properties.serializersByType);
this.serializersByCondition.putAll(properties.serializersByCondition);
this.formatter = properties.formatter; this.formatter = properties.formatter;
this.filter = properties.filter; this.filter = properties.filter;
this.outputNulls = properties.outputNulls; this.outputNulls = properties.outputNulls;
@ -131,6 +138,29 @@ class ConfigurationProperties {
return getThis(); 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<? super Type> 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 * Sets whether fields or collection elements whose value is null should be output
* while serializing the configuration. * while serializing the configuration.
@ -215,6 +245,18 @@ class ConfigurationProperties {
return serializersByType; 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<Predicate<? super Type>, Serializer<?, ?>> getSerializersByCondition() {
return serializersByCondition;
}
/** /**
* Returns whether null values should be output. * Returns whether null values should be output.
* *

@ -42,6 +42,9 @@ final class SerializerSelector {
} }
public Serializer<?, ?> select(Type type) { public Serializer<?, ?> select(Type type) {
final Serializer<?, ?> custom = selectCustomSerializer(type);
if (custom != null)
return custom;
if (type instanceof Class<?> cls) { if (type instanceof Class<?> cls) {
return selectForClass(cls); return selectForClass(cls);
} else if (type instanceof ParameterizedType pType) { } else if (type instanceof ParameterizedType pType) {
@ -60,9 +63,19 @@ final class SerializerSelector {
throw new ConfigurationException(baseExceptionMessage(type)); 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) { private Serializer<?, ?> selectForClass(Class<?> cls) {
if (properties.getSerializers().containsKey(cls))
return properties.getSerializers().get(cls);
if (DEFAULT_SERIALIZERS.containsKey(cls)) if (DEFAULT_SERIALIZERS.containsKey(cls))
return DEFAULT_SERIALIZERS.get(cls); return DEFAULT_SERIALIZERS.get(cls);
if (Reflect.isEnumType(cls)) { if (Reflect.isEnumType(cls)) {

@ -1,10 +1,13 @@
package de.exlll.configlib; package de.exlll.configlib;
import de.exlll.configlib.Serializers.StringSerializer;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.awt.Point; import java.awt.Point;
import java.lang.reflect.Type;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException; import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
@ -21,6 +24,7 @@ class ConfigurationPropertiesTest {
assertThat(properties.outputNulls(), is(false)); assertThat(properties.outputNulls(), is(false));
assertThat(properties.inputNulls(), is(false)); assertThat(properties.inputNulls(), is(false));
assertThat(properties.getSerializers().entrySet(), empty()); assertThat(properties.getSerializers().entrySet(), empty());
assertThat(properties.getSerializersByCondition().entrySet(), empty());
assertThat(properties.getFieldFormatter(), is(FieldFormatters.IDENTITY)); assertThat(properties.getFieldFormatter(), is(FieldFormatters.IDENTITY));
assertThat(properties.getFieldFilter(), is(FieldFilters.DEFAULT)); assertThat(properties.getFieldFilter(), is(FieldFilters.DEFAULT));
} }
@ -30,9 +34,11 @@ class ConfigurationPropertiesTest {
FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT); FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT);
FieldFilter filter = field -> field.getName().startsWith("f"); FieldFilter filter = field -> field.getName().startsWith("f");
TestUtils.PointSerializer serializer = new TestUtils.PointSerializer(); TestUtils.PointSerializer serializer = new TestUtils.PointSerializer();
Predicate<? super Type> predicate = type -> true;
ConfigurationProperties properties = ConfigurationProperties.newBuilder() ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, serializer) .addSerializer(Point.class, serializer)
.addSerializerByCondition(predicate, serializer)
.setFieldFormatter(formatter) .setFieldFormatter(formatter)
.setFieldFilter(filter) .setFieldFilter(filter)
.outputNulls(true) .outputNulls(true)
@ -41,6 +47,7 @@ class ConfigurationPropertiesTest {
.build(); .build();
assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer))); assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer)));
assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer)));
assertThat(properties.outputNulls(), is(true)); assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true)); assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false)); assertThat(properties.serializeSetsAsLists(), is(false));
@ -53,9 +60,11 @@ class ConfigurationPropertiesTest {
FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT); FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT);
FieldFilter filter = field -> field.getName().startsWith("f"); FieldFilter filter = field -> field.getName().startsWith("f");
TestUtils.PointSerializer serializer = new TestUtils.PointSerializer(); TestUtils.PointSerializer serializer = new TestUtils.PointSerializer();
Predicate<? super Type> predicate = type -> true;
ConfigurationProperties properties = ConfigurationProperties.newBuilder() ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, serializer) .addSerializer(Point.class, serializer)
.addSerializerByCondition(predicate, serializer)
.setFieldFormatter(formatter) .setFieldFormatter(formatter)
.setFieldFilter(filter) .setFieldFilter(filter)
.outputNulls(true) .outputNulls(true)
@ -66,6 +75,7 @@ class ConfigurationPropertiesTest {
.build(); .build();
assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer))); assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer)));
assertThat(properties.getSerializersByCondition(), is(Map.of(predicate, serializer)));
assertThat(properties.outputNulls(), is(true)); assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true)); assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false)); assertThat(properties.serializeSetsAsLists(), is(false));
@ -76,11 +86,16 @@ class ConfigurationPropertiesTest {
@Test @Test
void builderSerializersUnmodifiable() { void builderSerializersUnmodifiable() {
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build(); ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
Map<Class<?>, Serializer<?, ?>> map = properties.getSerializers(); var serializersByType = properties.getSerializers();
var serializersByCondition = properties.getSerializersByCondition();
assertThrows( assertThrows(
UnsupportedOperationException.class, 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 @Test
void addSerializerRequiresNonNull() { void addSerializerByTypeRequiresNonNull() {
assertThrowsNullPointerException( assertThrowsNullPointerException(
() -> builder.addSerializer(null, new Serializers.StringSerializer()), () -> builder.addSerializer(null, new StringSerializer()),
"serialized type" "serialized type"
); );
@ -115,5 +130,18 @@ class ConfigurationPropertiesTest {
"serializer" "serializer"
); );
} }
@Test
void addSerializerByConditionRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.addSerializerByCondition(null, new StringSerializer()),
"condition"
);
assertThrowsNullPointerException(
() -> builder.addSerializerByCondition(type -> true, null),
"serializer"
);
}
} }
} }

@ -178,7 +178,7 @@ class SerializerSelectorTest {
} }
@Test @Test
void selectSerializerCustomType() { void selectSerializerByCustomType() {
var properties = ConfigurationProperties.newBuilder() var properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, POINT_SERIALIZER) .addSerializer(Point.class, POINT_SERIALIZER)
.build(); .build();
@ -188,7 +188,7 @@ class SerializerSelectorTest {
} }
@Test @Test
void selectSerializerCustomSerializerTakesPrecedence() { void selectSerializerByCustomTypeTakesPrecedence() {
var properties = ConfigurationProperties.newBuilder() var properties = ConfigurationProperties.newBuilder()
.addSerializer(BigInteger.class, CUSTOM_BIG_INTEGER_SERIALIZER) .addSerializer(BigInteger.class, CUSTOM_BIG_INTEGER_SERIALIZER)
.build(); .build();
@ -198,6 +198,41 @@ class SerializerSelectorTest {
assertThat(bigIntegerSerializer, sameInstance(CUSTOM_BIG_INTEGER_SERIALIZER)); 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 @Test
void selectSerializerList() { void selectSerializerList() {
class A { class A {

@ -94,6 +94,28 @@ public final class TestUtils {
} }
} }
public static final class IdentifiableSerializer<S, T> implements Serializer<S, T> {
public int identifier;
public IdentifiableSerializer(int identifier) {
this.identifier = identifier;
}
public static IdentifiableSerializer<Integer, Integer> 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<Point, Point> { public static final class PointIdentitySerializer implements Serializer<Point, Point> {
@Override @Override
public Point serialize(Point element) { public Point serialize(Point element) {

@ -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<ConfigurationSerializable, String> {
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);
}
}

@ -1,8 +1,31 @@
package de.exlll.configlib; package de.exlll.configlib;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin; 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.
* <p>
* 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();
}
}

Loading…
Cancel
Save