ConfigLib v3

dev v3.0.0
Exlll 3 years ago
parent 65555f7dd6
commit 7c4f630f7c

@ -1,21 +1,18 @@
name: publish-on-master-push
name: publish-on-dispatch
on:
push:
branches: [ master ]
paths-ignore:
- '**/README.md'
on: [ 'workflow_dispatch' ]
jobs:
build-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 16
uses: actions/setup-java@v2
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup JDK and Gradle
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '16'
distribution: 'temurin'
java-version: '17'
- name: Build with Gradle
run: ./gradlew build
- name: Archive test reports

@ -12,12 +12,13 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 16
uses: actions/setup-java@v2
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup JDK and Gradle
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '16'
distribution: 'temurin'
java-version: '17'
- name: Test with Gradle
run: ./gradlew test
- name: Archive test reports

6
.gitignore vendored

@ -2,4 +2,8 @@
**/out
**/build
**/.gradle
**/.gradle
.data
TODO.txt
TMP.txt

@ -1,5 +0,0 @@
language: java
jdk:
- oraclejdk8
- oraclejdk9
- oraclejdk10

@ -1,5 +0,0 @@
package de.exlll.configlib;
import org.bukkit.plugin.java.JavaPlugin;
public class ConfigLib extends JavaPlugin {}

@ -1,56 +0,0 @@
package de.exlll.configlib.configs.yaml;
import org.bukkit.configuration.file.YamlConstructor;
import org.bukkit.configuration.file.YamlRepresenter;
import java.nio.file.Path;
/**
* A {@code BukkitYamlConfiguration} is a specialized form of a
* {@code YamlConfiguration} that uses better default values.
*/
public abstract class BukkitYamlConfiguration extends YamlConfiguration {
protected BukkitYamlConfiguration(Path path, BukkitYamlProperties properties) {
super(path, properties);
}
protected BukkitYamlConfiguration(Path path) {
this(path, BukkitYamlProperties.DEFAULT);
}
public static class BukkitYamlProperties extends YamlProperties {
public static final BukkitYamlProperties DEFAULT = builder().build();
private BukkitYamlProperties(Builder<?> builder) {
super(builder);
}
public static Builder<?> builder() {
return new Builder() {
@Override
protected Builder<?> getThis() {
return this;
}
};
}
public static abstract class
Builder<B extends BukkitYamlProperties.Builder<B>>
extends YamlProperties.Builder<B> {
protected Builder() {
setConstructor(new YamlConstructor());
setRepresenter(new YamlRepresenter());
}
/**
* Builds a new {@code BukkitYamlProperties} instance using the values set.
*
* @return new {@code BukkitYamlProperties} instance
*/
public BukkitYamlProperties build() {
return new BukkitYamlProperties(this);
}
}
}
}

@ -1,5 +0,0 @@
name: ConfigLib
author: Exlll
version: 2.2.0
main: de.exlll.configlib.ConfigLib

@ -1,5 +0,0 @@
package de.exlll.configlib;
import net.md_5.bungee.api.plugin.Plugin;
public class ConfigLib extends Plugin {}

@ -1,5 +0,0 @@
name: ConfigLib
author: Exlll
version: 2.2.0
main: de.exlll.configlib.ConfigLib

@ -1,85 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.Comment;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.toMap;
/**
* Instances of this class contain all comments of a {@link Configuration} class
* and its fields.
*/
public final class Comments {
private final List<String> classComments;
private final Map<String, List<String>> fieldComments;
private Comments(List<String> classComments,
Map<String, List<String>> fieldComments) {
this.classComments = classComments;
this.fieldComments = fieldComments;
}
static Comments ofClass(Class<?> cls) {
List<String> classComments = getComments(cls);
Map<String, List<String>> fieldComments = Arrays
.stream(cls.getDeclaredFields())
.filter(Comments::isCommented)
.collect(toMap(Field::getName, Comments::getComments));
return new Comments(classComments, fieldComments);
}
private static boolean isCommented(AnnotatedElement element) {
return element.isAnnotationPresent(Comment.class);
}
private static List<String> getComments(AnnotatedElement element) {
Comment comment = element.getAnnotation(Comment.class);
return (comment != null)
? Arrays.asList(comment.value())
: Collections.emptyList();
}
/**
* Returns if the {@code Configuration} this {@code Comments} object belongs to
* has class comments.
*
* @return true, if {@code Configuration} has class comments.
*/
public boolean hasClassComments() {
return !classComments.isEmpty();
}
/**
* Returns if the {@code Configuration} this {@code Comments} object belongs to
* has field comments.
*
* @return true, if {@code Configuration} has field comments.
*/
public boolean hasFieldComments() {
return !fieldComments.isEmpty();
}
/**
* Returns a list of class comments.
*
* @return list of class comments
*/
public List<String> getClassComments() {
return classComments;
}
/**
* Returns lists of field comments mapped by field name.
*
* @return lists of field comments by field name
*/
public Map<String, List<String>> getFieldComments() {
return fieldComments;
}
}

@ -1,214 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.FieldMapper.MappingInfo;
import de.exlll.configlib.filter.FieldFilter;
import de.exlll.configlib.filter.FieldFilters;
import de.exlll.configlib.format.FieldNameFormatter;
import de.exlll.configlib.format.FieldNameFormatters;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
/**
* Parent class of all configurations.
* <p>
* This class contains the most basic methods that every configuration needs.
*
* @param <C> type of the configuration
*/
public abstract class Configuration<C extends Configuration<C>> {
/**
* {@code Comments} object containing all class and field comments
* of this configuration
*/
protected final Comments comments;
private final Properties props;
/**
* Constructs a new {@code Configuration} object.
*
* @param properties {@code Properties} used to configure this configuration
* @throws NullPointerException if {@code properties} is null
*/
protected Configuration(Properties properties) {
this.props = Objects.requireNonNull(properties);
this.comments = Comments.ofClass(getClass());
}
/**
* Saves this {@code Configuration}.
*
* @throws ConfigurationException if any field is not properly configured
* @throws ConfigurationStoreException if an I/O error occurred while loading
* this configuration
*/
public final void save() {
try {
preSave();
MappingInfo mappingInfo = MappingInfo.from(this);
Map<String, Object> map = FieldMapper
.instanceToMap(this, mappingInfo);
getSource().saveConfiguration(getThis(), map);
} catch (IOException e) {
throw new ConfigurationStoreException(e);
}
}
/**
* Loads this {@code Configuration}.
*
* @throws ConfigurationException if values cannot be converted back to their
* original representation
* @throws ConfigurationStoreException if an I/O error occurred while loading
* this configuration
*/
public final void load() {
try {
Map<String, Object> map = getSource().loadConfiguration(getThis());
MappingInfo mappingInfo = MappingInfo.from(this);
FieldMapper.instanceFromMap(this, map, mappingInfo);
postLoad();
} catch (IOException e) {
throw new ConfigurationStoreException(e);
}
}
/**
* Returns the {@link ConfigurationSource} used for saving and loading this
* {@code Configuration}.
*
* @return {@code ConfigurationSource} used for saving and loading
*/
protected abstract ConfigurationSource<C> getSource();
/**
* Returns this {@code Configuration}.
*
* @return this {@code Configuration}
*/
protected abstract C getThis();
/**
* Hook that is executed right before this {@code Configuration} is saved.
* <p>
* The default implementation of this method does nothing.
*/
protected void preSave() {}
/**
* Hook that is executed right after this {@code Configuration} has
* successfully been loaded.
* <p>
* The default implementation of this method does nothing.
*/
protected void postLoad() {}
Properties getProperties() {
return props;
}
/**
* Instances of a {@code Properties} class are used to configure different
* aspects of a configuration.
*/
protected static class Properties {
private final FieldNameFormatter formatter;
private final FieldFilter filter;
/**
* Constructs a new {@code Properties} object.
*
* @param builder {@code Builder} used for construction
* @throws NullPointerException if {@code builder} is null
*/
protected Properties(Builder<?> builder) {
this.formatter = builder.formatter;
this.filter = builder.filter;
}
static Builder<?> builder() {
return new Builder() {
@Override
protected Builder<?> getThis() {
return this;
}
};
}
/**
* Returns the {@code FieldNameFormatter} of a configuration.
*
* @return {@code FieldNameFormatter} of a configuration
*/
public final FieldNameFormatter getFormatter() {
return formatter;
}
/**
* Returns the {@code FieldFilter} of a configuration
*
* @return {@code FieldFilter} of a configuration
*/
public final FieldFilter getFilter() {
return filter;
}
/**
* Builder classes are used for constructing {@code Properties}.
*
* @param <B> type of the builder
*/
protected static abstract class Builder<B extends Builder<B>> {
private FieldNameFormatter formatter = FieldNameFormatters.IDENTITY;
private FieldFilter filter = FieldFilters.DEFAULT;
protected Builder() {}
/**
* Returns this {@code Builder}.
*
* @return this {@code Builder}
*/
protected abstract B getThis();
/**
* Sets the {@link FieldNameFormatter} for a configuration.
*
* @param formatter formatter for configuration
* @return this {@code Builder}
* @throws NullPointerException if {@code formatter} is null
*/
public final B setFormatter(FieldNameFormatter formatter) {
this.formatter = Objects.requireNonNull(formatter);
return getThis();
}
/**
* Composes the given {@link FieldFilter} with the
* {@code FieldFilters.DEFAULT} instance and any other
* previously added filters.
* <p>
* The added filter is not evaluated for a field if the field has
* already been filtered or by some other {@code FieldFilter}.
*
* @param filter field filter that is added
* @return this {@code Builder}
* @throws NullPointerException if {@code filter} is null
*/
public final B addFilter(FieldFilter filter) {
this.filter = this.filter.and(filter);
return getThis();
}
/**
* Builds a new {@code Properties} instance using the values set.
*
* @return new {@code Properties} instance
*/
public Properties build() {
return new Properties(this);
}
}
}
}

@ -1,32 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.util.Map;
/**
* Implementations of this class save and load {@code Map<String, Object>} maps that
* represent converted configurations.
*
* @param <C> type of the configuration
*/
public interface ConfigurationSource<C extends Configuration<C>> {
/**
* Saves the given map.
*
* @param config the configuration that the {@code map} object represents
* @param map map that is saved
* @throws IOException if an I/O error occurs when saving the {@code map}
*/
void saveConfiguration(C config, Map<String, Object> map)
throws IOException;
/**
* Loads the map representing the given {@code Configuration}.
*
* @param config the configuration instance that requested the load
* @return map representing the given {@code Configuration}
* @throws IOException if an I/O error occurs when loading the map
*/
Map<String, Object> loadConfiguration(C config)
throws IOException;
}

@ -1,10 +0,0 @@
package de.exlll.configlib;
/**
* Signals that an error occurred while storing or loading a configuration.
*/
public final class ConfigurationStoreException extends RuntimeException {
public ConfigurationStoreException(Throwable cause) {
super(cause);
}
}

@ -1,231 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.FieldMapper.MappingInfo;
import de.exlll.configlib.annotation.ElementType;
import java.lang.reflect.Field;
/**
* Implementations of this interface convert field values to objects that can be
* stored by a {@link ConfigurationSource}, and vice versa.
* <p>
* Implementations must have a no-args constructor.
*
* @param <S> the source type
* @param <T> the target type
*/
public interface Converter<S, T> {
/**
* Converts a field value to an object that can be stored by a
* {@code ConfigurationSource}.
* <p>
* If this method returns null, a {@code ConfigurationException} is thrown.
*
* @param element field value that is converted
* @param info information about the current conversion step
* @return converted field value
*/
T convertTo(S element, ConversionInfo info);
/**
* Executes some action before the field value is converted.
*
* @param info information about the current conversion step
*/
default void preConvertTo(ConversionInfo info) {}
/**
* Converts a converted field value back to its original representation.
* <p>
* If this method returns null, the default value assigned to the field will
* be kept.
*
* @param element object that should be converted back
* @param info information about the current conversion step
* @return the element's original representation
*/
S convertFrom(T element, ConversionInfo info);
/**
* Executes some action before the converted field value is converted back
* to its original representation.
*
* @param info information about the current conversion step
*/
default void preConvertFrom(ConversionInfo info) {}
/**
* Instances of this class contain information about the currently converted
* configuration, configuration element, and the conversion step.
*/
final class ConversionInfo {
private final MappingInfo mappingInfo;
private final Field field;
private final Object instance;
private final Object value;
private final Object mapValue;
private final Class<?> fieldType;
private final Class<?> valueType;
private final Class<?> elementType;
private final String fieldName;
private final Configuration.Properties props;
private final int nestingLevel;
private int currentNestingLevel;
private ConversionInfo(
Field field, Object instance, Object mapValue,
MappingInfo mappingInfo
) {
this.mappingInfo = mappingInfo;
this.field = field;
this.instance = instance;
this.value = Reflect.getValue(field, instance);
this.mapValue = mapValue;
this.fieldType = field.getType();
this.valueType = value.getClass();
this.fieldName = field.getName();
this.props = mappingInfo.getProperties();
this.elementType = elementType(field);
this.nestingLevel = nestingLevel(field);
}
private static Class<?> elementType(Field field) {
if (field.isAnnotationPresent(ElementType.class)) {
ElementType et = field.getAnnotation(ElementType.class);
return et.value();
}
return null;
}
private static int nestingLevel(Field field) {
if (field.isAnnotationPresent(ElementType.class)) {
ElementType et = field.getAnnotation(ElementType.class);
return et.nestingLevel();
}
return -1;
}
static ConversionInfo from(
Field field, Object instance, MappingInfo mappingInfo
) {
return new ConversionInfo(field, instance, null, mappingInfo);
}
static ConversionInfo from(
Field field, Object instance, Object mapValue,
MappingInfo mappingInfo
) {
return new ConversionInfo(field, instance, mapValue, mappingInfo);
}
/**
* Returns the field all other values belong to.
*
* @return current field
*/
public Field getField() {
return field;
}
/**
* Returns the field name.
*
* @return current field name
*/
public String getFieldName() {
return fieldName;
}
/**
* Returns the object the field belongs to, i.e. the instance currently
* converted.
*
* @return object the field belongs to
*/
public Object getInstance() {
return instance;
}
/**
* Returns the default value assigned to that field.
*
* @return default value assigned to field
*/
public Object getValue() {
return value;
}
/**
* When loading, returns the converted field value, otherwise returns null.
*
* @return converted field value or null
*/
public Object getMapValue() {
return mapValue;
}
/**
* Returns the type of the field.
*
* @return field type
*/
public Class<?> getFieldType() {
return fieldType;
}
/**
* Returns the type of the default value assigned to the field.
*
* @return type default value assigned to field
*/
public Class<?> getValueType() {
return valueType;
}
/**
* Returns the {@code Configuration.Properties} instance of the currently
* converted configuration.
*
* @return properties of currently converted configuration
*/
public Configuration.Properties getProperties() {
return props;
}
/**
* Returns the value of the {@code ElementType} annotation or null if the
* field is not annotated with this annotation.
*
* @return value of the {@code ElementType} annotation or null
*/
public Class<?> getElementType() {
return elementType;
}
/**
* Returns whether the field is annotated with the {@code ElementType}
* annotation.
*
* @return true, if field is annotated with {@code ElementType}.
*/
public boolean hasElementType() {
return elementType != null;
}
int getNestingLevel() {
return nestingLevel;
}
int getCurrentNestingLevel() {
return currentNestingLevel;
}
void incCurrentNestingLevel() {
currentNestingLevel++;
}
MappingInfo getMappingInfo() {
return this.mappingInfo;
}
}
}

@ -1,551 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.Converter.ConversionInfo;
import de.exlll.configlib.annotation.Convert;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Function;
import static de.exlll.configlib.Validator.*;
import static java.util.stream.Collectors.*;
final class Converters {
private static final Map<Class<? extends Converter<?, ?>>, Converter<?, ?>> cache
= new WeakHashMap<>();
static final IdentityConverter IDENTITY_CONVERTER
= new IdentityConverter();
static final SimpleTypeConverter SIMPLE_TYPE_CONVERTER
= new SimpleTypeConverter();
static final EnumConverter ENUM_CONVERTER
= new EnumConverter();
static final ListConverter LIST_CONVERTER
= new ListConverter();
static final SetConverter SET_CONVERTER
= new SetConverter();
static final MapConverter MAP_CONVERTER
= new MapConverter();
static final SimpleListConverter SIMPLE_LIST_CONVERTER
= new SimpleListConverter();
static final SimpleSetConverter SIMPLE_SET_CONVERTER
= new SimpleSetConverter();
static final SimpleMapConverter SIMPLE_MAP_CONVERTER
= new SimpleMapConverter();
static final ConfigurationElementConverter ELEMENT_CONVERTER
= new ConfigurationElementConverter();
static Object convertTo(ConversionInfo info) {
Converter<Object, Object> converter = selectConverter(
info.getValueType(), info
);
converter.preConvertTo(info);
return tryConvertTo(converter, info);
}
private static Object tryConvertTo(
Converter<Object, Object> converter, ConversionInfo info
) {
try {
return converter.convertTo(info.getValue(), info);
} catch (ClassCastException e) {
String msg = "Converter '" + converter.getClass().getSimpleName() + "'" +
" cannot convert value '" + info.getValue() + "' of field '" +
info.getFieldName() + "' because it expects a different type.";
throw new ConfigurationException(msg, e);
}
}
static Object convertFrom(ConversionInfo info) {
Converter<Object, Object> converter = selectConverter(
info.getValueType(), info
);
converter.preConvertFrom(info);
return tryConvertFrom(converter, info);
}
private static Object tryConvertFrom(
Converter<Object, Object> converter, ConversionInfo info
) {
try {
return converter.convertFrom(info.getMapValue(), info);
} catch (ClassCastException | IllegalArgumentException e) {
String msg = "The value for field '" + info.getFieldName() + "' with " +
"type '" + getClsName(info.getFieldType()) + "' cannot " +
"be converted back to its original representation because a " +
"type mismatch occurred.";
throw new ConfigurationException(msg, e);
}
}
private static String getClsName(Class<?> cls) {
return cls.getSimpleName();
}
private static Converter<Object, Object> selectConverter(
Class<?> valueType, ConversionInfo info
) {
Converter<?, ?> converter;
if (Reflect.hasNoConvert(info.getField())) {
converter = IDENTITY_CONVERTER;
} else if (Reflect.hasConverter(info.getField())) {
converter = instantiateConverter(info.getField());
} else if (Reflect.isSimpleType(valueType)) {
converter = SIMPLE_TYPE_CONVERTER;
} else {
converter = selectNonSimpleConverter(valueType, info);
}
return toObjectConverter(converter);
}
private static Converter<Object, Object> selectNonSimpleConverter(
Class<?> valueType, ConversionInfo info
) {
Converter<?, ?> converter;
if (Reflect.isEnumType(valueType) ||
/* type is a string when converting back */
(valueType == String.class)) {
converter = ENUM_CONVERTER;
} else if (Reflect.isContainerType(valueType)) {
converter = selectContainerConverter(valueType, info);
} else {
converter = ELEMENT_CONVERTER;
}
return toObjectConverter(converter);
}
private static Converter<?, ?> instantiateConverter(Field field) {
Convert convert = field.getAnnotation(Convert.class);
return cache.computeIfAbsent(convert.value(), cls -> {
checkConverterHasNoArgsConstructor(cls, field.getName());
return Reflect.newInstance(cls);
});
}
private static Converter<?, ?> selectContainerConverter(
Class<?> valueType, ConversionInfo info
) {
if (info.hasElementType()) {
return selectElementTypeContainerConverter(valueType);
} else {
return selectSimpleContainerConverter(valueType);
}
}
private static Converter<?, ?> selectElementTypeContainerConverter(
Class<?> valueType
) {
return selector(
LIST_CONVERTER, SET_CONVERTER, MAP_CONVERTER
).apply(valueType);
}
private static Converter<?, ?> selectSimpleContainerConverter(
Class<?> valueType
) {
return selector(
SIMPLE_LIST_CONVERTER, SIMPLE_SET_CONVERTER, SIMPLE_MAP_CONVERTER
).apply(valueType);
}
static <R> Function<Class<?>, R> selector(R listValue, R setValue, R mapValue) {
return containerClass -> {
if (List.class.isAssignableFrom(containerClass)) {
return listValue;
} else if (Set.class.isAssignableFrom(containerClass)) {
return setValue;
} else {
return mapValue;
}
};
}
static String selectContainerName(Class<?> containerType) {
return selector("list", "set", "map").apply(containerType);
}
private static Converter<Object, Object> toObjectConverter(
Converter<?, ?> converter
) {
/* This cast may result in a ClassCastException when converting objects
* back to their original representation. This happens if the type of the
* converted object has changed for some reason (e.g. by a configuration
* mistake). However, the ClassCastException is later caught and translated
* to a ConfigurationException to give additional information about what
* happened. */
@SuppressWarnings("unchecked")
Converter<Object, Object> c = (Converter<Object, Object>) converter;
return c;
}
private static final class SimpleListConverter
implements Converter<List<?>, List<?>> {
@Override
public List<?> convertTo(List<?> element, ConversionInfo info) {
return element;
}
@Override
public void preConvertTo(ConversionInfo info) {
checkContainerValuesNotNull(info);
checkContainerValuesSimpleType(info);
}
@Override
public List<?> convertFrom(List<?> element, ConversionInfo info) {
return element;
}
}
private static final class SimpleSetConverter
implements Converter<Set<?>, Set<?>> {
@Override
public Set<?> convertTo(Set<?> element, ConversionInfo info) {
return element;
}
@Override
public void preConvertTo(ConversionInfo info) {
checkContainerValuesNotNull(info);
checkContainerValuesSimpleType(info);
}
@Override
public Set<?> convertFrom(Set<?> element, ConversionInfo info) {
return element;
}
}
private static final class SimpleMapConverter
implements Converter<Map<?, ?>, Map<?, ?>> {
@Override
public Map<?, ?> convertTo(Map<?, ?> element, ConversionInfo info) {
return element;
}
@Override
public void preConvertTo(ConversionInfo info) {
checkMapKeysAndValues(info);
checkContainerValuesSimpleType(info);
}
@Override
public Map<?, ?> convertFrom(Map<?, ?> element, ConversionInfo info) {
return element;
}
}
private static final class ListConverter
implements Converter<List<?>, List<?>> {
@Override
public List<?> convertTo(List<?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.get(0);
Function<Object, ?> f = createToConversionFunction(o, info);
return element.stream().map(f).collect(toList());
}
@Override
public void preConvertTo(ConversionInfo info) {
checkElementType(info);
checkContainerValuesNotNull(info);
checkContainerTypes(info);
}
@Override
public List<?> convertFrom(List<?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.get(0);
Function<Object, ?> f = createFromConversionFunction(o, info);
return element.stream().map(f).collect(toList());
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkElementType(info);
}
}
private static final class SetConverter
implements Converter<Set<?>, Set<?>> {
@Override
public Set<?> convertTo(Set<?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.iterator().next();
Function<Object, ?> f = createToConversionFunction(o, info);
return element.stream().map(f).collect(toSet());
}
@Override
public void preConvertTo(ConversionInfo info) {
checkElementType(info);
checkContainerValuesNotNull(info);
checkContainerTypes(info);
}
@Override
public Set<?> convertFrom(Set<?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.iterator().next();
Function<Object, ?> f = createFromConversionFunction(o, info);
return element.stream().map(f).collect(toSet());
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkElementType(info);
}
}
private static final class MapConverter
implements Converter<Map<?, ?>, Map<?, ?>> {
@Override
public Map<?, ?> convertTo(Map<?, ?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.values().iterator().next();
Function<Object, ?> cf = createToConversionFunction(o, info);
Function<Map.Entry<?, ?>, ?> f = e -> cf.apply(e.getValue());
return element.entrySet().stream().collect(toMap(Map.Entry::getKey, f));
}
@Override
public void preConvertTo(ConversionInfo info) {
checkElementType(info);
checkMapKeysAndValues(info);
checkContainerTypes(info);
}
@Override
public Map<?, ?> convertFrom(Map<?, ?> element, ConversionInfo info) {
if (element.isEmpty()) {
return element;
}
Object o = element.values().iterator().next();
Function<Object, ?> cf = createFromConversionFunction(o, info);
Function<Map.Entry<?, ?>, ?> f = e -> cf.apply(e.getValue());
return element.entrySet().stream().collect(toMap(Map.Entry::getKey, f));
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkElementType(info);
}
}
private static Function<Object, ?> createToConversionFunction(
Object element, ConversionInfo info
) {
checkNestingLevel(element, info);
if (Reflect.isContainerType(element.getClass())) {
info.incCurrentNestingLevel();
}
Converter<Object, ?> converter = selectNonSimpleConverter(
element.getClass(), info
);
return o -> converter.convertTo(o, info);
}
private static Function<Object, ?> createFromConversionFunction(
Object element, ConversionInfo info
) {
boolean currentLevelSameAsExpected =
info.getNestingLevel() == info.getCurrentNestingLevel();
checkCurrentLevelSameAsExpectedRequiresMapOrString(
currentLevelSameAsExpected, element, info
);
if ((element instanceof Map<?, ?>) && currentLevelSameAsExpected) {
return o -> {
Map<String, Object> map = toTypeMap(o, null);
Object inst = Reflect.newInstance(info.getElementType());
FieldMapper.instanceFromMap(inst, map, info.getMappingInfo());
return inst;
};
} else if ((element instanceof String) && currentLevelSameAsExpected) {
return createNonSimpleConverter(element, info);
} else {
info.incCurrentNestingLevel();
return createNonSimpleConverter(element, info);
}
}
private static Function<Object, ?> createNonSimpleConverter(
Object element, ConversionInfo info
) {
Converter<?, Object> converter = selectNonSimpleConverter(
element.getClass(), info
);
return o -> converter.convertFrom(o, info);
}
private static Map<String, Object> toTypeMap(Object value, String fn) {
checkIsMap(value, fn);
checkMapKeysAreStrings((Map<?, ?>) value, fn);
// The following cast won't fail because we just verified that
// it's a Map<String, Object>.
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) value;
return map;
}
private static final class IdentityConverter
implements Converter<Object, Object> {
@Override
public Object convertTo(Object element, ConversionInfo info) {
return element;
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
return element;
}
}
private static final class SimpleTypeConverter
implements Converter<Object, Object> {
@Override
public Object convertTo(Object element, ConversionInfo info) {
return element;
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
if (info.getFieldType() == element.getClass()) {
return element;
}
if (element instanceof Number) {
return convertNumber(info.getFieldType(), (Number) element);
}
if (element instanceof String) {
return convertString((String) element);
}
return element;
}
private Object convertNumber(Class<?> target, Number value) {
if (target == byte.class || target == Byte.class) {
return value.byteValue();
} else if (target == short.class || target == Short.class) {
return value.shortValue();
} else if (target == int.class || target == Integer.class) {
return value.intValue();
} else if (target == long.class || target == Long.class) {
return value.longValue();
} else if (target == float.class || target == Float.class) {
return value.floatValue();
} else if (target == double.class || target == Double.class) {
return value.doubleValue();
} else {
String msg = "Number '" + value + "' cannot be converted " +
"to type '" + target + "'";
throw new IllegalArgumentException(msg);
}
}
private Object convertString(String s) {
int length = s.length();
if (length == 0) {
String msg = "An empty string cannot be converted to a character.";
throw new IllegalArgumentException(msg);
}
if (length > 1) {
String msg = "String '" + s + "' is too long to " +
"be converted to a character";
throw new IllegalArgumentException(msg);
}
return s.charAt(0);
}
}
private static final class EnumConverter
implements Converter<Enum<?>, String> {
@Override
public String convertTo(Enum<?> element, ConversionInfo info) {
return element.toString();
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkEnumValueIsString(info);
}
@Override
public Enum<?> convertFrom(String element, ConversionInfo info) {
Class<? extends Enum> cls = getEnumClass(info);
try {
/* cast won't fail because we know that it's an enum */
@SuppressWarnings("unchecked")
Enum<?> enm = Enum.valueOf(cls, element);
return enm;
} catch (IllegalArgumentException e) {
checkElementTypeIsEnumType(cls, info);
String in = selectWord(info);
String msg = "Cannot initialize " + in + " because there is no " +
"enum constant '" + element + "'.\nValid constants are: " +
Arrays.toString(cls.getEnumConstants());
throw new IllegalArgumentException(msg, e);
}
}
private String selectWord(ConversionInfo info) {
String fn = info.getFieldName();
if (Reflect.isContainerType(info.getFieldType())) {
String w = selectContainerName(info.getValueType());
return "an enum element of " + w + " '" + fn + "'";
}
return "enum '" + fn + "' ";
}
@SuppressWarnings("unchecked")
private Class<? extends Enum> getEnumClass(ConversionInfo info) {
/* this cast won't fail because this method is only called by a
* Converter that converts enum types. */
return (Class<? extends Enum>) (!info.hasElementType()
? info.getValue().getClass()
: info.getElementType());
}
}
private static final class ConfigurationElementConverter
implements Converter<Object, Object> {
@Override
public Object convertTo(Object element, ConversionInfo info) {
return FieldMapper.instanceToMap(element, info.getMappingInfo());
}
@Override
public void preConvertTo(ConversionInfo info) {
checkTypeIsConfigurationElement(info.getValueType(), info.getFieldName());
checkTypeHasNoArgsConstructor(info);
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
checkElementIsConvertibleToConfigurationElement(element, info);
Object newInstance = Reflect.newInstance(info.getValueType());
Map<String, Object> typeMap = toTypeMap(element, info.getFieldName());
FieldMapper.instanceFromMap(newInstance, typeMap, info.getMappingInfo());
return newInstance;
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkTypeHasNoArgsConstructor(info);
checkTypeIsConfigurationElement(info.getValueType(), info.getFieldName());
}
}
}

@ -1,120 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.Converter.ConversionInfo;
import de.exlll.configlib.annotation.Format;
import de.exlll.configlib.filter.FieldFilter;
import de.exlll.configlib.format.FieldNameFormatter;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import static de.exlll.configlib.Validator.*;
enum FieldMapper {
;
static Map<String, Object> instanceToMap(Object inst, MappingInfo mappingInfo) {
Map<String, Object> map = new LinkedHashMap<>();
Configuration.Properties props = mappingInfo.getProperties();
FieldFilter filter = props.getFilter();
for (Field field : filter.filterDeclaredFieldsOf(inst.getClass())) {
Object val = toConvertibleObject(field, inst, mappingInfo);
FieldNameFormatter fnf = selectFormatter(mappingInfo);
String fn = fnf.fromFieldName(field.getName());
map.put(fn, val);
}
return map;
}
private static Object toConvertibleObject(
Field field, Object instance, MappingInfo mappingInfo
) {
checkDefaultValueNull(field, instance);
ConversionInfo info = ConversionInfo.from(field, instance, mappingInfo);
checkFieldWithElementTypeIsContainer(info);
Object converted = Converters.convertTo(info);
checkConverterNotReturnsNull(converted, info);
return converted;
}
static void instanceFromMap(
Object inst, Map<String, Object> instMap, MappingInfo mappingInfo
) {
FieldFilter filter = mappingInfo.getProperties().getFilter();
for (Field field : filter.filterDeclaredFieldsOf(inst.getClass())) {
FieldNameFormatter fnf = selectFormatter(mappingInfo);
String fn = fnf.fromFieldName(field.getName());
Object mapValue = instMap.get(fn);
if (mapValue != null) {
fromConvertedObject(field, inst, mapValue, mappingInfo);
}
}
}
private static void fromConvertedObject(
Field field, Object instance, Object mapValue,
MappingInfo mappingInfo
) {
checkDefaultValueNull(field, instance);
ConversionInfo info = ConversionInfo.from(
field, instance, mapValue, mappingInfo
);
checkFieldWithElementTypeIsContainer(info);
Object convert = Converters.convertFrom(info);
if (convert == null) {
return;
}
if (Reflect.isContainerType(info.getFieldType())) {
checkFieldTypeAssignableFrom(convert.getClass(), info);
}
Reflect.setValue(field, instance, convert);
}
private static void checkDefaultValueNull(Field field, Object instance) {
Object val = Reflect.getValue(field, instance);
checkNotNull(val, field.getName());
}
static FieldNameFormatter selectFormatter(MappingInfo info) {
Configuration<?> configuration = info.getConfiguration();
Configuration.Properties props = info.getProperties();
if ((configuration != null) &&
Reflect.hasFormatter(configuration.getClass())) {
Format format = configuration.getClass()
.getAnnotation(Format.class);
return (format.formatterClass() != FieldNameFormatter.class)
? Reflect.newInstance(format.formatterClass())
: format.value();
}
return props.getFormatter();
}
static final class MappingInfo {
private final Configuration<?> configuration;
private final Configuration.Properties properties;
MappingInfo(
Configuration<?> configuration,
Configuration.Properties properties
) {
this.configuration = configuration;
this.properties = properties;
}
Configuration<?> getConfiguration() {
return configuration;
}
Configuration.Properties getProperties() {
return properties;
}
static MappingInfo from(Configuration<?> configuration) {
return new MappingInfo(configuration, configuration.getProperties());
}
}
}

@ -1,110 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.ConfigurationElement;
import de.exlll.configlib.annotation.Convert;
import de.exlll.configlib.annotation.Format;
import de.exlll.configlib.annotation.NoConvert;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
enum Reflect {
;
private static final Set<Class<?>> SIMPLE_TYPES = new HashSet<>(Arrays.asList(
Boolean.class,
Byte.class,
Character.class,
Short.class,
Integer.class,
Long.class,
Float.class,
Double.class,
String.class
));
static boolean isSimpleType(Class<?> cls) {
return cls.isPrimitive() || SIMPLE_TYPES.contains(cls);
}
static boolean isContainerType(Class<?> cls) {
return List.class.isAssignableFrom(cls) ||
Set.class.isAssignableFrom(cls) ||
Map.class.isAssignableFrom(cls);
}
static boolean isEnumType(Class<?> cls) {
return cls.isEnum();
}
static <T> T newInstance(Class<T> cls) {
try {
Constructor<T> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (NoSuchMethodException e) {
String msg = "Class " + cls.getSimpleName() + " doesn't have a " +
"no-args constructor.";
throw new ConfigurationException(msg, e);
} catch (IllegalAccessException e) {
/* This exception should not be thrown because
* we set the field to be accessible. */
String msg = "No-args constructor of class " + cls.getSimpleName() +
" not accessible.";
throw new ConfigurationException(msg, e);
} catch (InstantiationException e) {
String msg = "Class " + cls.getSimpleName() + " not instantiable.";
throw new ConfigurationException(msg, e);
} catch (InvocationTargetException e) {
String msg = "Constructor of class " + cls.getSimpleName() +
" has thrown an exception.";
throw new ConfigurationException(msg, e);
}
}
static Object getValue(Field field, Object inst) {
try {
field.setAccessible(true);
return field.get(inst);
} catch (IllegalAccessException e) {
/* This exception should not be thrown because
* we set the field to be accessible. */
String msg = "Illegal access of field '" + field + "' " +
"on object " + inst + ".";
throw new ConfigurationException(msg, e);
}
}
static void setValue(Field field, Object inst, Object value) {
try {
field.setAccessible(true);
field.set(inst, value);
} catch (IllegalAccessException e) {
String msg = "Illegal access of field '" + field + "' " +
"on object " + inst + ".";
throw new ConfigurationException(msg, e);
}
}
static boolean hasConverter(Field field) {
return field.isAnnotationPresent(Convert.class);
}
static boolean hasNoConvert(Field field) {
return field.isAnnotationPresent(NoConvert.class);
}
static boolean hasFormatter(Class<?> cls) {
return cls.isAnnotationPresent(Format.class);
}
static boolean isConfigurationElement(Class<?> cls) {
return cls.isAnnotationPresent(ConfigurationElement.class);
}
static boolean hasNoArgConstructor(Class<?> cls) {
return Arrays.stream(cls.getDeclaredConstructors())
.anyMatch(c -> c.getParameterCount() == 0);
}
}

@ -1,357 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.Converter.ConversionInfo;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class Validator {
static void checkNotNull(Object o, String fn) {
if (o == null) {
String msg = "The value of field '" + fn + "' is null.\n" +
"Please assign a non-null default value or remove this field.";
throw new ConfigurationException(msg);
}
}
static void checkContainerTypes(ConversionInfo info) {
Object value = info.getValue();
Collection<?> collection = toCollection(value);
checkCollectionTypes(collection, info);
}
private static void checkCollectionTypes(
Collection<?> collection, ConversionInfo info
) {
for (Object element : collection) {
if (Reflect.isContainerType(element.getClass())) {
Collection<?> container = toCollection(element);
checkCollectionTypes(container, info);
} else {
checkCollectionType(element, info);
}
}
}
private static void checkCollectionType(Object element, ConversionInfo info) {
Class<?> cls = element.getClass();
if (cls != info.getElementType()) {
String cNameField = selectContainerNameField(info);
String cValues = selectContainerValues(info);
String msg = "The type of " + cNameField + " doesn't match the " +
"type indicated by the ElementType annotation.\n" +
"Required type: '" + getClsName(info.getElementType()) +
"'\tActual type: '" + getClsName(cls) +
"'\n" + cValues;
throw new ConfigurationException(msg);
}
}
private static String selectContainerValues(ConversionInfo info) {
Object value = info.getValue();
return Converters.selector(
"All elements: " + value,
"All elements: " + value,
"All entries: " + value
).apply(info.getValueType());
}
private static String selectContainerNameField(ConversionInfo info) {
String fieldName = info.getFieldName();
return Converters.selector(
"an element of list '" + fieldName + "'",
"an element of set '" + fieldName + "'",
"a value of map '" + fieldName + "'"
).apply(info.getValueType());
}
private static Collection<?> toCollection(Object container) {
if (container instanceof List<?> || container instanceof Set<?>) {
return (Collection<?>) container;
} else {
Map<?, ?> map = (Map<?, ?>) container;
return map.values();
}
}
static void checkMapKeysAndValues(ConversionInfo info) {
checkMapKeysSimple((Map<?, ?>) info.getValue(), info.getFieldName());
checkContainerValuesNotNull(info);
}
private static void checkMapKeysSimple(Map<?, ?> map, String fn) {
for (Object o : map.keySet()) {
if (!Reflect.isSimpleType(o.getClass())) {
String msg = "The keys of map '" + fn + "' must be simple types.";
throw new ConfigurationException(msg);
}
}
}
static void checkContainerValuesNotNull(ConversionInfo info) {
Collection<?> collection = toCollection(info.getValue());
checkCollectionValuesNotNull(collection, info);
}
private static void checkCollectionValuesNotNull(
Collection<?> col, ConversionInfo info
) {
for (Object element : col) {
checkCollectionValueNotNull(element, info);
if (Reflect.isContainerType(element.getClass())) {
Collection<?> container = toCollection(element);
checkCollectionValuesNotNull(container, info);
}
}
}
private static void checkCollectionValueNotNull(
Object element, ConversionInfo info
) {
if (element == null) {
String cnf = selectContainerNameField(info)
.replaceFirst("a", "A");
String msg = cnf + " is null.\n" +
"Please either remove or replace this element." +
"\n" + selectContainerValues(info);
throw new ConfigurationException(msg);
}
}
static void checkContainerValuesSimpleType(ConversionInfo info) {
Collection<?> collection = toCollection(info.getValue());
checkCollectionValuesSimpleType(collection, info);
}
private static void checkCollectionValuesSimpleType(
Collection<?> collection, ConversionInfo info
) {
for (Object element : collection) {
if (Reflect.isContainerType(element.getClass())) {
Collection<?> elements = toCollection(element);
checkCollectionValuesSimpleType(elements, info);
} else {
checkCollectionValueSimpleType(element, info);
}
}
}
private static void checkCollectionValueSimpleType(
Object element, ConversionInfo info
) {
if (!Reflect.isSimpleType(element.getClass())) {
String cn = Converters.selectContainerName(info.getValueType());
String cnf = selectContainerNameField(info);
String fieldName = info.getFieldName();
String msg = "The type of " + cnf + " is not a simple type but " + cn +
" '" + fieldName + "' is missing the ElementType annotation." +
"\n" + selectContainerValues(info);
throw new ConfigurationException(msg);
}
}
static void checkTypeIsConfigurationElement(Class<?> cls, String fn) {
if (!Reflect.isConfigurationElement(cls)) {
String msg = "Type '" + getClsName(cls) + "' of field '" +
fn + "' is not annotated as a configuration element.";
throw new ConfigurationException(msg);
}
}
private static String getClsName(Class<?> cls) {
String clsName = cls.getSimpleName();
if (clsName.equals("")) {
clsName = cls.getName();
}
return clsName;
}
static void checkIsMap(Object value, String fn) {
Class<?> cls = value.getClass();
if (!Map.class.isAssignableFrom(cls)) {
String msg = "Initializing field '" + fn + "' requires a " +
"Map<String, Object> but the given object is not a map.\n" +
"Type: '" + cls.getSimpleName() + "'\tValue: '" + value + "'";
throw new ConfigurationException(msg);
}
}
static void checkMapKeysAreStrings(Map<?, ?> map, String fn) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
Object key = entry.getKey();
if ((key == null) || (key.getClass() != String.class)) {
String msg = "Initializing field '" + fn + "' requires a " +
"Map<String, Object> but the given map contains " +
"non-string keys.\nAll entries: " + map;
throw new ConfigurationException(msg);
}
}
}
static void checkElementType(ConversionInfo info) {
Class<?> elementType = info.getElementType();
if (!elementType.isEnum())
checkElementTypeIsConfigurationElement(info);
checkElementTypeIsConcrete(info);
if (!elementType.isEnum())
checkElementTypeHasNoArgsConstructor(info);
}
static void checkFieldWithElementTypeIsContainer(ConversionInfo info) {
boolean isContainer = Reflect.isContainerType(info.getValueType());
if (info.hasElementType() && !isContainer) {
String msg = "Field '" + info.getFieldName() + "' is annotated with " +
"the ElementType annotation but is not a List, Set or Map.";
throw new ConfigurationException(msg);
}
}
private static void checkElementTypeIsConfigurationElement(ConversionInfo info) {
Class<?> elementType = info.getElementType();
if (!Reflect.isConfigurationElement(elementType)) {
String msg = "The element type '" + getClsName(elementType) + "'" +
" of field '" + info.getFieldName() + "' is not a " +
"configuration element.";
throw new ConfigurationException(msg);
}
}
private static void checkElementTypeIsConcrete(ConversionInfo info) {
Class<?> elementType = info.getElementType();
String msg = getType(elementType);
if (msg != null) {
msg = "The element type of field '" + info.getFieldName() + "' must " +
"be a concrete class but type '" +
getClsName(elementType) + "' is " + msg;
throw new ConfigurationException(msg);
}
}
private static String getType(Class<?> cls) {
String msg = null;
if (cls.isInterface()) {
msg = "an interface.";
} else if (cls.isPrimitive()) {
msg = "primitive.";
} else if (cls.isArray()) {
msg = "an array.";
} else if (Modifier.isAbstract(cls.getModifiers())) {
msg = "an abstract class.";
}
return msg;
}
private static void checkElementTypeHasNoArgsConstructor(ConversionInfo info) {
Class<?> elementType = info.getElementType();
if (!Reflect.hasNoArgConstructor(elementType)) {
String msg = "The element type '" + elementType.getSimpleName() + "'" +
" of field '" + info.getFieldName() + "' doesn't have " +
"a no-args constructor.";
throw new ConfigurationException(msg);
}
}
static void checkTypeHasNoArgsConstructor(ConversionInfo info) {
Class<?> valueType = info.getValueType();
if (!Reflect.hasNoArgConstructor(valueType)) {
String msg = "Type '" + getClsName(valueType) + "' of field '" +
info.getFieldName() + "' doesn't have a no-args constructor.";
throw new ConfigurationException(msg);
}
}
static void checkConverterHasNoArgsConstructor(Class<?> converterClass, String fn) {
if (!Reflect.hasNoArgConstructor(converterClass)) {
String msg = "Converter '" + converterClass.getSimpleName() + "' used " +
"on field '" + fn + "' doesn't have a no-args constructor.";
throw new ConfigurationException(msg);
}
}
static void checkEnumValueIsString(ConversionInfo info) {
Object val = info.getMapValue();
if (!(val instanceof String)) {
String sn = val.getClass().getSimpleName();
String msg = "Initializing enum '" + info.getFieldName() + "' " +
"requires a string but '" + val + "' is of type '" + sn + "'.";
throw new ConfigurationException(msg);
}
}
static void checkFieldTypeAssignableFrom(Class<?> type, ConversionInfo info) {
Class<?> fieldType = info.getFieldType();
if (!fieldType.isAssignableFrom(type)) {
String msg = "Can not set field '" + info.getFieldName() + "' with " +
"type '" + getClsName(fieldType) + "' to '" +
getClsName(type) + "'.";
throw new ConfigurationException(msg);
}
}
static void checkElementIsConvertibleToConfigurationElement(
Object element, ConversionInfo info
) {
Class<?> eClass = element.getClass();
if (Reflect.isContainerType(info.getFieldType()) &&
!Map.class.isAssignableFrom(eClass)) {
String msg = "Initializing field '" + info.getFieldName() + "' " +
"requires objects of type Map<String, Object> but element " +
"'" + element + "' is of type '" + getClsName(eClass) + "'.";
throw new IllegalArgumentException(msg);
}
}
static void checkNestingLevel(Object element, ConversionInfo info) {
if (!Reflect.isContainerType(element.getClass())) {
if (info.getNestingLevel() != info.getCurrentNestingLevel()) {
String msg = "Field '" + info.getFieldName() + "' of class " +
"'" + getClsName(info.getInstance().getClass()) + "' " +
"has a nesting level of " + info.getNestingLevel() +
" but the first object of type '" +
getClsName(info.getElementType()) + "' was found on " +
"level " + info.getCurrentNestingLevel() + ".";
throw new ConfigurationException(msg);
}
}
}
static void checkCurrentLevelSameAsExpectedRequiresMapOrString(
boolean currentLevelSameAsExpected,
Object element, ConversionInfo info
) {
boolean isMapOrString = (element instanceof Map<?, ?>) ||
(element instanceof String);
if (currentLevelSameAsExpected && !isMapOrString) {
Class<?> cls = info.getInstance().getClass();
String msg = "Field '" + info.getFieldName() + "' of class '" +
getClsName(cls) + "' has a nesting level" +
" of " + info.getNestingLevel() + " but element '" + element +
"' of type '" + getClsName(element.getClass()) + "' cannot be " +
"converted to '" + getClsName(info.getElementType()) + "'.";
throw new ConfigurationException(msg);
}
}
static void checkElementTypeIsEnumType(Class<?> type, ConversionInfo info) {
if (!Reflect.isEnumType(type)) {
String msg = "Element type '" + getClsName(type) + "' of field " +
"'" + info.getFieldName() + "' is not an enum type.";
throw new IllegalArgumentException(msg);
}
}
static void checkConverterNotReturnsNull(Object converted, ConversionInfo info) {
if (converted == null) {
String msg = "Failed to convert value '" + info.getValue() + "' of " +
"field '" + info.getFieldName() + "' because the converter " +
"returned null.";
throw new ConfigurationException(msg);
}
}
}

@ -1,32 +0,0 @@
package de.exlll.configlib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated element is saved together with explanatory
* comments describing it.
* <p>
* For {@link de.exlll.configlib.configs.yaml.YamlConfiguration YamlConfiguration}s:
* <ul>
* <li>
* If this annotation is used on a class, the comments returned by the
* {@link #value()} method are saved at the beginning of the configuration file.
* </li>
* <li>
* If this annotation is used on a field, the comments are saved above the field name.
* </li>
* </ul>
*/
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Comment {
/**
* Returns the comments of the annotated type or field.
*
* @return class or field comments
*/
String[] value();
}

@ -1,15 +0,0 @@
package de.exlll.configlib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Enables conversion of instances of the annotated type.
* <p>
* {@code ConfigurationElement}s must have a no-args constructor.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigurationElement {}

@ -1,18 +0,0 @@
package de.exlll.configlib.annotation;
import de.exlll.configlib.Converter;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that a custom conversion mechanism is used to convert the
* annotated field.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Convert {
Class<? extends Converter<?, ?>> value();
}

@ -1,40 +0,0 @@
package de.exlll.configlib.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates the type of elements a {@code Collection} or {@code Map} contains.
* <p>
* This annotation must only be used if a {@code Collection} or {@code Map} contains
* elements whose type is not simple. Note that {@code Map} keys can only be of some
* simple type.
* <p>
* If collections are nested, the {@code nestingLevel} must be set. Examples:
* <ul>
* <li>nestingLevel 1: {@code List<List<T>>}</li>
* <li>nestingLevel 1: {@code List<Set<T>>}</li>
* <li>nestingLevel 1: {@code List<Map<String, T>>}</li>
* <li>nestingLevel 2: {@code List<List<List<T>>>}</li>
* <li>nestingLevel 2: {@code List<Set<List<T>>>}</li>
* <li>nestingLevel 2: {@code List<List<Map<String, T>>>}</li>
* </ul>
*/
@Target(java.lang.annotation.ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ElementType {
/**
* Returns the type of elements a {@code Collection} or {@code Map} contains.
*
* @return type of elements.
*/
Class<?> value();
/**
* Returns the nesting level
*
* @return nesting level
*/
int nestingLevel() default 0;
}

@ -1,38 +0,0 @@
package de.exlll.configlib.annotation;
import de.exlll.configlib.format.FieldNameFormatter;
import de.exlll.configlib.format.FieldNameFormatters;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that a specific {@code FieldNameFormatter} is used. If a
* {@link #formatterClass()} is specified, the {@code FieldNameFormatters}
* returned by {@link #value()} is ignored. The {@code formatterClass} must
* be instantiable and must have a no-args constructor.
* <p>
* This annotation takes precedence over the value set in properties object.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Format {
/**
* Returns the {@code FieldNameFormatters} instance to be for formatting.
* The return value of this method is ignored if a {@code formatterClass()}
* is set.
*
* @return {@code FieldNameFormatters} instance
*/
FieldNameFormatters value() default FieldNameFormatters.IDENTITY;
/**
* Returns the class of a {@code FieldNameFormatter} implementation.
* The class must be instantiable and must have a no-args constructor.
*
* @return class of {@code FieldNameFormatter} implementation
*/
Class<? extends FieldNameFormatter> formatterClass() default FieldNameFormatter.class;
}

@ -1,17 +0,0 @@
package de.exlll.configlib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated field should not be converted but instead used as is.
* <p>
* This may be useful if the configuration knows how to (de-)serialize
* instances of that type. For example, a {@code BukkitYamlConfiguration}
* knows how to serialize {@code ItemStack} instances.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoConvert {}

@ -1,76 +0,0 @@
package de.exlll.configlib.configs.yaml;
import de.exlll.configlib.Comments;
import de.exlll.configlib.format.FieldNameFormatter;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
final class YamlComments {
private final Comments comments;
YamlComments(Comments comments) {
this.comments = comments;
}
String classCommentsAsString() {
List<String> classComments = comments.getClassComments();
return commentListToString(classComments);
}
Map<String, String> fieldCommentAsStrings(FieldNameFormatter formatter) {
Map<String, List<String>> fieldComments = comments.getFieldComments();
return fieldComments.entrySet().stream()
.map(e -> toFormattedStringCommentEntry(e, formatter))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Map.Entry<String, String> toFormattedStringCommentEntry(
Map.Entry<String, List<String>> entry, FieldNameFormatter formatter
) {
String fieldComments = commentListToString(entry.getValue());
String formattedKey = formatter.fromFieldName(entry.getKey());
return new MapEntry<>(formattedKey, fieldComments);
}
private String commentListToString(List<String> comments) {
return comments.stream()
.map(this::toCommentLine)
.collect(joining("\n"));
}
private String toCommentLine(String comment) {
return comment.isEmpty() ? "" : "# " + comment;
}
private static final class MapEntry<K, V> implements Map.Entry<K, V> {
private final K key;
private V value;
public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
V old = this.value;
this.value = value;
return old;
}
}
}

@ -1,212 +0,0 @@
package de.exlll.configlib.configs.yaml;
import de.exlll.configlib.Comments;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.ConfigurationSource;
import de.exlll.configlib.ConfigurationStoreException;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public abstract class YamlConfiguration extends Configuration<YamlConfiguration> {
private final YamlSource source;
protected YamlConfiguration(Path path, YamlProperties properties) {
super(properties);
this.source = new YamlSource(path, properties);
}
protected YamlConfiguration(Path path) {
this(path, YamlProperties.DEFAULT);
}
@Override
protected final ConfigurationSource<YamlConfiguration> getSource() {
return source;
}
@Override
protected final YamlConfiguration getThis() {
return this;
}
public final void loadAndSave() {
try {
load();
save();
} catch (ConfigurationStoreException e) {
if (e.getCause() instanceof NoSuchFileException) {
postLoad();
save();
} else {
throw e;
}
}
}
Comments getComments() {
return comments;
}
public static class YamlProperties extends Properties {
public static final YamlProperties DEFAULT = builder().build();
private final List<String> prependedComments;
private final List<String> appendedComments;
private final BaseConstructor constructor;
private final Representer representer;
private final DumperOptions options;
private final Resolver resolver;
protected YamlProperties(Builder<?> builder) {
super(builder);
this.prependedComments = builder.prependedComments;
this.appendedComments = builder.appendedComments;
this.constructor = builder.constructor;
this.representer = builder.representer;
this.options = builder.options;
this.resolver = builder.resolver;
}
public static Builder<?> builder() {
return new Builder() {
@Override
protected Builder<?> getThis() {
return this;
}
};
}
public final List<String> getPrependedComments() {
return prependedComments;
}
public final List<String> getAppendedComments() {
return appendedComments;
}
public final BaseConstructor getConstructor() {
return constructor;
}
public final Representer getRepresenter() {
return representer;
}
public final DumperOptions getOptions() {
return options;
}
public final Resolver getResolver() {
return resolver;
}
public static abstract class Builder<B extends Builder<B>>
extends Properties.Builder<B> {
private List<String> prependedComments = Collections.emptyList();
private List<String> appendedComments = Collections.emptyList();
private BaseConstructor constructor = new Constructor();
private Representer representer = new Representer();
private DumperOptions options = new DumperOptions();
private Resolver resolver = new Resolver();
protected Builder() {
options.setIndent(2);
options.setDefaultFlowStyle(FlowStyle.BLOCK);
}
/**
* Sets the comments prepended to a configuration.
*
* @param prependedComments List of comments that are prepended
* @return this {@code Builder}
* @throws NullPointerException if {@code prependedComments ist null}
*/
public final B setPrependedComments(List<String> prependedComments) {
this.prependedComments = Objects.requireNonNull(prependedComments);
return getThis();
}
/**
* Sets the comments appended to a configuration.
*
* @param appendedComments List of comments that are appended
* @return this {@code Builder}
* @throws NullPointerException if {@code appendedComments ist null}
*/
public final B setAppendedComments(List<String> appendedComments) {
this.appendedComments = Objects.requireNonNull(appendedComments);
return getThis();
}
/**
* Sets the {@link BaseConstructor} used by the underlying YAML-parser.
*
* @param constructor {@code BaseConstructor} used by YAML-parser.
* @return this {@code Builder}
* @throws NullPointerException if {@code constructor ist null}
* @see <a href="https://bitbucket.org/asomov/snakeyaml/wiki/Documentation">snakeyaml-Documentation</a>
*/
public final B setConstructor(BaseConstructor constructor) {
this.constructor = Objects.requireNonNull(constructor);
return getThis();
}
/**
* Sets the {@link Representer} used by the underlying YAML-parser.
*
* @param representer {@code Representer} used by YAML-parser.
* @return this {@code Builder}
* @throws NullPointerException if {@code representer ist null}
* @see <a href="https://bitbucket.org/asomov/snakeyaml/wiki/Documentation">snakeyaml-Documentation</a>
*/
public final B setRepresenter(Representer representer) {
this.representer = Objects.requireNonNull(representer);
return getThis();
}
/**
* Sets the {@link DumperOptions} used by the underlying YAML-parser.
*
* @param options {@code DumperOptions} used by YAML-parser.
* @return this {@code Builder}
* @throws NullPointerException if {@code options ist null}
* @see <a href="https://bitbucket.org/asomov/snakeyaml/wiki/Documentation">snakeyaml-Documentation</a>
*/
public final B setOptions(DumperOptions options) {
this.options = Objects.requireNonNull(options);
return getThis();
}
/**
* Sets the {@link Resolver} used by the underlying YAML-parser.
*
* @param resolver {@code Resolver} used by YAML-parser.
* @return this {@code Builder}
* @throws NullPointerException if {@code resolver ist null}
* @see <a href="https://bitbucket.org/asomov/snakeyaml/wiki/Documentation">snakeyaml-Documentation</a>
*/
public final B setResolver(Resolver resolver) {
this.resolver = Objects.requireNonNull(resolver);
return getThis();
}
/**
* Builds a new {@code YamlProperties} instance using the values set.
*
* @return new {@code YamlProperties} instance
*/
public YamlProperties build() {
return new YamlProperties(this);
}
}
}
}

@ -1,138 +0,0 @@
package de.exlll.configlib.configs.yaml;
import de.exlll.configlib.Comments;
import de.exlll.configlib.ConfigurationSource;
import de.exlll.configlib.configs.yaml.YamlConfiguration.YamlProperties;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.stream.Collectors.joining;
final class YamlSource implements ConfigurationSource<YamlConfiguration> {
private final Path configPath;
private final YamlProperties props;
private final Yaml yaml;
public YamlSource(Path configPath, YamlProperties props) {
this.configPath = Objects.requireNonNull(configPath);
this.props = props;
this.yaml = new Yaml(
props.getConstructor(), props.getRepresenter(),
props.getOptions(), props.getResolver()
);
}
@Override
public void saveConfiguration(YamlConfiguration config, Map<String, Object> map)
throws IOException {
createParentDirectories();
CommentAdder adder = new CommentAdder(
yaml.dump(map), config.getComments(), props
);
String commentedDump = adder.getCommentedDump();
Files.write(configPath, commentedDump.getBytes());
}
private void createParentDirectories() throws IOException {
Path parentDir = configPath.getParent();
if (!Files.isDirectory(parentDir)) {
Files.createDirectories(parentDir);
}
}
@Override
public Map<String, Object> loadConfiguration(YamlConfiguration config)
throws IOException {
String cfg = readConfig();
return yaml.load(cfg);
}
private String readConfig() throws IOException {
return Files.lines(configPath).collect(joining("\n"));
}
private static final class CommentAdder {
private static final Pattern PREFIX_PATTERN = Pattern.compile("^\\w+:.*");
private final String dump;
private final Comments comments;
private final YamlComments yamlComments;
private final YamlProperties props;
private final StringBuilder builder;
private CommentAdder(String dump, Comments comments,
YamlProperties props
) {
this.dump = dump;
this.props = props;
this.comments = comments;
this.yamlComments = new YamlComments(comments);
this.builder = new StringBuilder(dump.length());
}
public String getCommentedDump() {
addComments(props.getPrependedComments());
addClassComments();
addFieldComments();
addComments(props.getAppendedComments());
return builder.toString();
}
private void addComments(List<String> comments) {
for (String comment : comments) {
if (!comment.isEmpty()) {
builder.append("# ").append(comment);
}
builder.append('\n');
}
}
private void addClassComments() {
if (comments.hasClassComments()) {
builder.append(yamlComments.classCommentsAsString());
builder.append("\n");
}
}
private void addFieldComments() {
if (comments.hasFieldComments()) {
List<String> dumpLines = Arrays.asList(dump.split("\n"));
addDumpLines(dumpLines);
} else {
builder.append(dump);
}
}
private void addDumpLines(List<String> dumpLines) {
for (String dumpLine : dumpLines) {
Matcher m = PREFIX_PATTERN.matcher(dumpLine);
if (m.matches()) {
addFieldComment(dumpLine);
}
builder.append(dumpLine).append('\n');
}
}
private void addFieldComment(String dumpLine) {
Map<String, String> map = yamlComments.fieldCommentAsStrings(
props.getFormatter()
);
for (Map.Entry<String, String> entry : map.entrySet()) {
String prefix = entry.getKey() + ":";
if (dumpLine.startsWith(prefix)) {
builder.append(entry.getValue()).append('\n');
break;
}
}
}
}
}

@ -1,26 +0,0 @@
package de.exlll.configlib.filter;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toList;
@FunctionalInterface
public interface FieldFilter extends Predicate<Field> {
@Override
default FieldFilter and(Predicate<? super Field> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default List<? extends Field> filterDeclaredFieldsOf(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
return Arrays.stream(fields)
.filter(this)
.collect(toList());
}
}

@ -1,20 +0,0 @@
package de.exlll.configlib.filter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
public enum FieldFilters implements FieldFilter {
DEFAULT {
@Override
public boolean test(Field field) {
if (field.isSynthetic()) {
return false;
}
int mods = field.getModifiers();
return !(Modifier.isFinal(mods) ||
Modifier.isStatic(mods) ||
Modifier.isTransient(mods));
}
}
}

@ -1,13 +0,0 @@
package de.exlll.configlib.format;
import java.util.function.Function;
@FunctionalInterface
public interface FieldNameFormatter extends Function<String, String> {
String fromFieldName(String fieldName);
@Override
default String apply(String s) {
return fromFieldName(s);
}
}

@ -1,55 +0,0 @@
package de.exlll.configlib.format;
public enum FieldNameFormatters implements FieldNameFormatter {
/**
* Represents a {@code FieldNameFormatter} that doesn't actually format the
* field name but instead returns it.
*/
IDENTITY {
@Override
public String fromFieldName(String fn) {
return fn;
}
},
/**
* Represents a {@code FieldNameFormatter} that transforms <i>camelCase</i> to
* <i>lower_underscore</i>.
* <p>
* For example, <i>myPrivateField</i> becomes <i>my_private_field</i>.
*/
LOWER_UNDERSCORE {
@Override
public String fromFieldName(String fn) {
StringBuilder builder = new StringBuilder(fn.length());
for (char c : fn.toCharArray()) {
if (Character.isLowerCase(c)) {
builder.append(c);
} else if (Character.isUpperCase(c)) {
c = Character.toLowerCase(c);
builder.append('_').append(c);
}
}
return builder.toString();
}
},
/**
* Represents a {@code FieldNameFormatter} that transforms <i>camelCase</i> to
* <i>UPPER_UNDERSCORE</i>.
* <p>
* For example, <i>myPrivateField</i> becomes <i>MY_PRIVATE_FIELD</i>.
*/
UPPER_UNDERSCORE {
@Override
public String fromFieldName(String fieldName) {
StringBuilder builder = new StringBuilder(fieldName.length());
for (char c : fieldName.toCharArray()) {
if (Character.isLowerCase(c)) {
builder.append(Character.toUpperCase(c));
} else if (Character.isUpperCase(c)) {
builder.append('_').append(c);
}
}
return builder.toString();
}
}
}

@ -1,54 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.Comment;
import org.junit.jupiter.api.Test;
import static de.exlll.configlib.util.CollectionFactory.listOf;
import static de.exlll.configlib.util.CollectionFactory.mapOf;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class CommentsTest {
@Test
void classCommentsAdded() {
class A {}
@Comment("B")
class B {}
@Comment({"C", "D"})
class C {}
Comments comments = Comments.ofClass(A.class);
assertThat(comments.getClassComments(), empty());
assertThat(comments.getFieldComments().entrySet(), empty());
comments = Comments.ofClass(B.class);
assertThat(comments.getClassComments(), is(listOf("B")));
assertThat(comments.getFieldComments().entrySet(), empty());
comments = Comments.ofClass(C.class);
assertThat(comments.getClassComments(), is(listOf("C", "D")));
assertThat(comments.getFieldComments().entrySet(), empty());
}
@Test
void fieldCommentsAdded() {
class A {
int a;
@Comment("b")
int b;
@Comment({"c", "d"})
int c;
}
Comments comments = Comments.ofClass(A.class);
assertThat(comments.getClassComments(), empty());
assertThat(comments.getFieldComments(), is(mapOf(
"b", listOf("b"),
"c", listOf("c", "d")
)));
}
}

@ -1,47 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.configs.mem.InSharedMemoryConfiguration;
import org.junit.jupiter.api.Test;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
class ConfigurationTest {
private static class TestHook extends InSharedMemoryConfiguration {
protected TestHook() { super(Properties.builder().build()); }
}
@Test
void configExecutesPreSaveHook() {
class A extends TestHook {
int i = 0;
@Override
protected void preSave() { i++; }
}
A save = new A();
save.save();
assertThat(save.i, is(1));
A load = new A();
load.load();
assertThat(load.i, is(1));
}
@Test
void configExecutesPostLoadHook() {
class A extends TestHook {
int i = 0;
@Override
protected void postLoad() { i++; }
}
A save = new A();
save.save();
assertThat(save.i, is(0));
A load = new A();
load.load();
assertThat(load.i, is(1));
}
}

@ -1,392 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.Convert;
import de.exlll.configlib.annotation.ElementType;
import de.exlll.configlib.classes.TestSubClass;
import de.exlll.configlib.classes.TestSubClassConverter;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static de.exlll.configlib.FieldMapperHelpers.*;
import static de.exlll.configlib.util.CollectionFactory.listOf;
import static de.exlll.configlib.util.CollectionFactory.mapOf;
import static de.exlll.configlib.util.CollectionFactory.setOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@SuppressWarnings({"unused", "ThrowableNotThrown"})
public class FieldMapperConverterTest {
private static class Point2D {
protected int x = 1;
protected int y = 2;
private Point2D() {}
protected Point2D(int x, int y) {
this.x = x;
this.y = y;
}
private static Point2D of(int x, int y) {
return new Point2D(x, y);
}
}
private static final class PointToListConverter
implements Converter<Point2D, List<Integer>> {
@Override
public List<Integer> convertTo(Point2D element, ConversionInfo info) {
return listOf(element.x, element.y);
}
@Override
public Point2D convertFrom(List<Integer> element, ConversionInfo info) {
Point2D point = new Point2D();
point.x = element.get(0);
point.y = element.get(1);
return point;
}
}
private static final class PointToMapConverter
implements Converter<Point2D, Map<String, String>> {
@Override
public Map<String, String> convertTo(Point2D element, ConversionInfo info) {
int x = element.x;
int y = element.y;
return mapOf("p", x + ":" + y);
}
@Override
public Point2D convertFrom(Map<String, String> element, ConversionInfo info) {
String p = element.get("p");
String[] split = p.split(":");
return Point2D.of(Integer.valueOf(split[0]), Integer.valueOf(split[1]));
}
}
private static final class IntToStringConverter
implements Converter<Integer, String> {
@Override
public String convertTo(Integer element, ConversionInfo info) {
return element.toString();
}
@Override
public Integer convertFrom(String element, ConversionInfo info) {
return Integer.valueOf(element);
}
}
@Test
void instanceToMapRequiresNoArgsConverter() {
class A {
@Convert(MultiArgsConverter.class)
int i;
}
class B {
@Convert(SubConverter.class)
int i;
}
class C {
@Convert(EnumConverter.class)
int i;
}
String msg = "Converter 'MultiArgsConverter' used on field 'i' doesn't " +
"have a no-args constructor.";
assertItmCfgExceptionMessage(new A(), msg);
msg = "Converter 'SubConverter' used on field 'i' doesn't " +
"have a no-args constructor.";
assertItmCfgExceptionMessage(new B(), msg);
msg = "Converter 'EnumConverter' used on field 'i' doesn't " +
"have a no-args constructor.";
assertItmCfgExceptionMessage(new C(), msg);
}
private static final class MultiArgsConverter
implements Converter<Object, Object> {
private MultiArgsConverter(int i) {}
@Override
public Object convertTo(Object element, ConversionInfo info) {
return null;
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
return null;
}
}
private static final class NullConverter
implements Converter<String, String> {
@Override
public String convertTo(String element, ConversionInfo info) {
return null;
}
@Override
public String convertFrom(String element, ConversionInfo info) {
return null;
}
}
@Test
void instanceToMapThrowsExceptionIfConverterReturnsNull() {
class A {
@Convert(NullConverter.class)
String s = "string";
}
String msg = "Failed to convert value 'string' of field 's' because " +
"the converter returned null.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceFromMapKeepsDefaultValueIfConverterReturnsNull() {
class A {
@Convert(NullConverter.class)
String s = "string";
}
Map<String, Object> map = mapOf("s", "value");
A a = instanceFromMap(new A(), map);
assertThat(a.s, is("string"));
}
private interface SubConverter extends Converter<Object, Object> {}
private enum EnumConverter implements Converter<Object, Object> {
;
@Override
public Object convertTo(Object element, ConversionInfo info) {
return null;
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
return null;
}
}
@Test
void instanceToMapUsesConverterForSimpleTypes() {
class A {
@Convert(IntToStringConverter.class)
int i = 1;
}
Map<String, Object> map = instanceToMap(new A());
assertThat(map.get("i"), is("1"));
}
@Test
void instanceFromMapUsesConverterForSimpleTypes() {
class A {
@Convert(IntToStringConverter.class)
int i = 1;
}
A i = instanceFromMap(new A(), mapOf("i", "10"));
assertThat(i.i, is(10));
}
@Test
void instanceToMapConvertsCustomTypesUsingConverters() {
class A {
@Convert(PointToListConverter.class)
Point2D p1 = new Point2D();
@Convert(PointToMapConverter.class)
Point2D p2 = new Point2D();
}
Map<String, Object> map = instanceToMap(new A());
assertThat(map.get("p1"), is(listOf(1, 2)));
assertThat(map.get("p2"), is(mapOf("p", "1:2")));
}
@Test
void instanceFromMapConvertsCustomTypesUsingConverters() {
class A {
@Convert(PointToListConverter.class)
Point2D p1 = new Point2D();
@Convert(PointToMapConverter.class)
Point2D p2 = new Point2D();
}
Map<String, Object> map = mapOf(
"p1", listOf(10, 11),
"p2", mapOf("p", "11:12")
);
A i = instanceFromMap(new A(), map);
assertThat(i.p1.x, is(10));
assertThat(i.p1.y, is(11));
assertThat(i.p2.x, is(11));
assertThat(i.p2.y, is(12));
}
private static final class CountingConverter
implements Converter<Object, Object> {
static int instanceCount;
public CountingConverter() {
instanceCount++;
}
@Override
public Object convertTo(Object element, ConversionInfo info) {
return element;
}
@Override
public Object convertFrom(Object element, ConversionInfo info) {
return element;
}
}
@Test
void convertersUseCache() {
class A {
@Convert(CountingConverter.class)
Point2D a = new Point2D();
@Convert(CountingConverter.class)
Point2D b = new Point2D();
}
Map<String, Object> map = instanceToMap(new A());
assertThat(CountingConverter.instanceCount, is(1));
instanceFromMap(new A(), map);
assertThat(CountingConverter.instanceCount, is(1));
}
@Test
void instanceToMapCatchesClassCastException() {
class A {
@Convert(TestSubClassConverter.class)
String s = "string";
}
String msg = "Converter 'TestSubClassConverter' cannot convert value " +
"'string' of field 's' because it expects a different type.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceFromMapCatchesClassCastExceptionOfCustomClasses() {
class A {
@Convert(TestSubClassConverter.class)
TestSubClass a = new TestSubClass();
}
Map<String, Object> map = mapOf(
"a", 1
);
String msg = "The value for field 'a' with type 'TestSubClass' " +
"cannot be converted back to its original representation because " +
"a type mismatch occurred.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapCatchesClassCastExceptionOfChars() {
class C {
char c;
}
class D {
char d;
}
Map<String, Object> map = mapOf(
"c", "", "d", "12"
);
String msg = "The value for field 'c' with type 'char' " +
"cannot be converted back to its original representation because " +
"a type mismatch occurred.";
assertIfmCfgExceptionMessage(new C(), map, msg);
msg = "The value for field 'd' with type 'char' " +
"cannot be converted back to its original representation because " +
"a type mismatch occurred.";
assertIfmCfgExceptionMessage(new D(), map, msg);
}
@Test
void instanceFromMapCatchesClassCastExceptionOfStrings() {
class B {
String b = "string";
}
Map<String, Object> map = mapOf(
"b", 2
);
String msg = "The value for field 'b' with type 'String' " +
"cannot be converted back to its original representation because " +
"a type mismatch occurred.";
assertIfmCfgExceptionMessage(new B(), map, msg);
}
@Test
void instanceFromMapCatchesClassCastExceptionOfUnknownEnumConstants() {
class A {
LocalTestEnum e = LocalTestEnum.T;
}
Map<String, Object> map = mapOf(
"e", "V"
);
String msg = "The value for field 'e' with type 'LocalTestEnum' " +
"cannot be converted back to its original representation because " +
"a type mismatch occurred.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapCatchesClassCastExceptionOfUnknownEnumConstantsInLists() {
class A {
@ElementType(value = LocalTestEnum.class, nestingLevel = 1)
List<List<LocalTestEnum>> l = listOf();
}
Map<String, Object> map = mapOf(
"l", listOf(listOf("Q", "V"))
);
ConfigurationException ex = assertIfmThrowsCfgException(new A(), map);
Throwable cause = ex.getCause();
String msg = "Cannot initialize an enum element of list 'l' because there " +
"is no enum constant 'Q'.\nValid constants are: [S, T]";
MatcherAssert.assertThat(cause.getMessage(), is(msg));
}
@Test
void instanceFromMapCatchesClassCastExceptionOfUnknownEnumConstantsInSets() {
class A {
@ElementType(value = LocalTestEnum.class, nestingLevel = 1)
Set<List<LocalTestEnum>> s = setOf();
}
Map<String, Object> map = mapOf(
"s", setOf(listOf("Q", "V"))
);
ConfigurationException ex = assertIfmThrowsCfgException(new A(), map);
Throwable cause = ex.getCause();
String msg = "Cannot initialize an enum element of set 's' because there " +
"is no enum constant 'Q'.\nValid constants are: [S, T]";
MatcherAssert.assertThat(cause.getMessage(), is(msg));
}
@Test
void instanceFromMapCatchesClassCastExceptionOfUnknownEnumConstantsInMaps() {
class A {
@ElementType(value = LocalTestEnum.class, nestingLevel = 1)
Map<Integer, List<LocalTestEnum>> m = mapOf();
}
Map<String, Object> map = mapOf(
"m", mapOf(1, listOf("Q", "V"))
);
ConfigurationException ex = assertIfmThrowsCfgException(new A(), map);
Throwable cause = ex.getCause();
String msg = "Cannot initialize an enum element of map 'm' because there " +
"is no enum constant 'Q'.\nValid constants are: [S, T]";
MatcherAssert.assertThat(cause.getMessage(), is(msg));
}
}

@ -1,91 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.FieldMapper.MappingInfo;
import de.exlll.configlib.annotation.ConfigurationElement;
import java.util.Map;
import static de.exlll.configlib.configs.yaml.YamlConfiguration.YamlProperties.DEFAULT;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class FieldMapperHelpers {
@ConfigurationElement
interface LocalTestInterface {}
@ConfigurationElement
static class LocalTestInterfaceImpl implements LocalTestInterface {}
@ConfigurationElement
static abstract class LocalTestAbstractClass {}
@ConfigurationElement
static class LocalTestAbstractClassImpl extends LocalTestAbstractClass {}
@ConfigurationElement
enum LocalTestEnum {
S, T
}
static class Sub1 {}
@ConfigurationElement
static class Sub2 {}
@ConfigurationElement
static class Sub3 {
public Sub3(int x) {}
}
public static void assertItmCfgExceptionMessage(Object o, String msg) {
ConfigurationException ex = assertItmThrowsCfgException(o);
assertThat(ex.getMessage(), is(msg));
}
public static void assertIfmCfgExceptionMessage(
Object o, Map<String, Object> map, String msg
) {
ConfigurationException ex = assertIfmThrowsCfgException(o, map);
assertThat(ex.getMessage(), is(msg));
}
public static ConfigurationException assertItmThrowsCfgException(Object o) {
return assertThrows(
ConfigurationException.class,
() -> instanceToMap(o)
);
}
public static ConfigurationException assertIfmThrowsCfgException(
Object o, Map<String, Object> map
) {
return assertThrows(
ConfigurationException.class,
() -> instanceFromMap(o, map)
);
}
public static Map<String, Object> instanceToMap(Object o) {
return instanceToMap(o, DEFAULT);
}
public static Map<String, Object> instanceToMap(
Object o, Configuration.Properties props
) {
MappingInfo mappingInfo = new MappingInfo(null, props);
return FieldMapper.instanceToMap(o, mappingInfo);
}
public static <T> T instanceFromMap(T o, Map<String, Object> map) {
return instanceFromMap(o, map, DEFAULT);
}
public static <T> T instanceFromMap(
T o, Map<String, Object> map, Configuration.Properties props
) {
MappingInfo mappingInfo = new MappingInfo(null, props);
FieldMapper.instanceFromMap(o, map, mappingInfo);
return o;
}
}

@ -1,592 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.Converter.ConversionInfo;
import de.exlll.configlib.FieldMapper.MappingInfo;
import de.exlll.configlib.annotation.ElementType;
import de.exlll.configlib.annotation.Format;
import de.exlll.configlib.annotation.NoConvert;
import de.exlll.configlib.classes.TestClass;
import de.exlll.configlib.classes.TestSubClass;
import de.exlll.configlib.classes.TestSubClassConverter;
import de.exlll.configlib.configs.mem.InSharedMemoryConfiguration;
import de.exlll.configlib.configs.yaml.YamlConfiguration.YamlProperties;
import de.exlll.configlib.format.FieldNameFormatter;
import de.exlll.configlib.format.FieldNameFormatters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static de.exlll.configlib.Converters.ENUM_CONVERTER;
import static de.exlll.configlib.Converters.SIMPLE_TYPE_CONVERTER;
import static de.exlll.configlib.FieldMapperHelpers.*;
import static de.exlll.configlib.util.CollectionFactory.*;
import static java.util.stream.Collectors.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SuppressWarnings({"unused", "ThrowableNotThrown"})
class FieldMapperTest {
private static final TestClass t = TestClass.TEST_VALUES;
private static final Configuration.Properties DEFAULT =
Configuration.Properties.builder().build();
private static final YamlProperties WITH_UPPER_FORMATTER = YamlProperties
.builder()
.setFormatter(FieldNameFormatters.UPPER_UNDERSCORE)
.build();
private static final Map<String, Object> map = instanceToMap(
TestClass.TEST_VALUES, DEFAULT
);
private TestClass tmp;
@BeforeEach
void setUp() {
tmp = new TestClass();
MappingInfo mappingInfo = MappingInfo.from(tmp);
FieldMapper.instanceFromMap(tmp, map, mappingInfo);
}
@Test
void instanceFromMapSetsSimpleTypes() {
assertAll(
() -> assertThat(tmp.getPrimBool(), is(t.getPrimBool())),
() -> assertThat(tmp.getRefBool(), is(t.getRefBool())),
() -> assertThat(tmp.getPrimByte(), is(t.getPrimByte())),
() -> assertThat(tmp.getRefByte(), is(t.getRefByte())),
() -> assertThat(tmp.getPrimChar(), is(t.getPrimChar())),
() -> assertThat(tmp.getRefChar(), is(t.getRefChar())),
() -> assertThat(tmp.getPrimShort(), is(t.getPrimShort())),
() -> assertThat(tmp.getRefShort(), is(t.getRefShort())),
() -> assertThat(tmp.getPrimInt(), is(t.getPrimInt())),
() -> assertThat(tmp.getRefInt(), is(t.getRefInt())),
() -> assertThat(tmp.getPrimLong(), is(t.getPrimLong())),
() -> assertThat(tmp.getRefLong(), is(t.getRefLong())),
() -> assertThat(tmp.getPrimFloat(), is(t.getPrimFloat())),
() -> assertThat(tmp.getRefFloat(), is(t.getRefFloat())),
() -> assertThat(tmp.getPrimDouble(), is(t.getPrimDouble())),
() -> assertThat(tmp.getRefDouble(), is(t.getRefDouble())),
() -> assertThat(tmp.getString(), is(t.getString()))
);
}
@Test
void instanceFromMapSetsEnums() {
assertThat(tmp.getE1(), is(t.getE1()));
}
@Test
void instanceFromMapSetsContainersOfSimpleTypes() {
assertAll(
() -> assertThat(tmp.getInts(), is(t.getInts())),
() -> assertThat(tmp.getStrings(), is(t.getStrings())),
() -> assertThat(tmp.getDoubleByBool(), is(t.getDoubleByBool()))
);
}
@Test
void instanceFromMapsConvertsMapsToTypes() {
assertThat(tmp.getSubClass(), is(t.getSubClass()));
}
@Test
void instanceFromMapsConvertsExcludedClasses() {
assertThat(tmp.getExcludedClass(), is(t.getExcludedClass()));
}
@Test
void instanceFromMapsConvertsContainersOfMaps() {
assertThat(tmp.getSubClassList(), is(t.getSubClassList()));
assertThat(tmp.getSubClassSet(), is(t.getSubClassSet()));
assertThat(tmp.getSubClassMap(), is(t.getSubClassMap()));
assertThat(tmp.getSubClassListsList(), is(t.getSubClassListsList()));
assertThat(tmp.getSubClassSetsSet(), is(t.getSubClassSetsSet()));
assertThat(tmp.getSubClassMapsMap(), is(t.getSubClassMapsMap()));
}
@Test
void instanceFromMapDoesNotSetFinalStaticOrTransientFields() {
Map<String, Object> map = mapOf(
"staticFinalInt", 10,
"staticInt", 10,
"finalInt", 10,
"transientInt", 10
);
TestClass cls = instanceFromMap(new TestClass(), map);
assertThat(TestClass.getStaticFinalInt(), is(1));
assertThat(TestClass.getStaticInt(), is(2));
assertThat(cls.getFinalInt(), is(3));
assertThat(cls.getTransientInt(), is(4));
}
@Test
void instanceFromMapConvertsAllFields() {
assertThat(tmp, is(t));
}
@Test
void instanceFromMapThrowsExceptionIfEnumConstantDoesNotExist() {
class A {
LocalTestEnum t = LocalTestEnum.T;
}
Map<String, Object> map = mapOf(
"t", "R"
);
String msg = "Cannot initialize enum 't' because there is no enum " +
"constant 'R'.\nValid constants are: [S, T]";
assertThrows(
IllegalArgumentException.class,
() -> ((Converter<Enum<?>, String>) ENUM_CONVERTER)
.convertFrom("R", newInfo("t", new A())),
msg
);
}
@Test
void instanceFromMapIgnoresNullValues() {
class A {
TestSubClass c = new TestSubClass();
}
Map<String, Object> map = mapOf("c", mapOf("primInt", 20));
A a = new A();
assertThat(a.c.getPrimInt(), is(0));
assertThat(a.c.getString(), is(""));
instanceFromMap(a, map);
assertThat(a.c.getPrimInt(), is(20));
assertThat(a.c.getString(), is(""));
}
@Test
void instanceFromMapSetsField() {
TestClass ins = TestClass.TEST_VALUES;
TestClass def = new TestClass();
assertThat(ins, is(not(def)));
instanceFromMap(def, map);
assertThat(ins, is(def));
}
@Test
void instanceFromMapCreatesConcreteInstances() {
class A {
LocalTestInterface l = new LocalTestInterfaceImpl();
}
class B {
LocalTestAbstractClass l = new LocalTestAbstractClassImpl();
}
instanceFromMap(new A(), mapOf("l", mapOf()));
instanceFromMap(new B(), mapOf("l", mapOf()));
}
@Test
void instanceToMapUsesFieldNameFormatter() {
Configuration.Properties.Builder builder =
Configuration.Properties.builder();
Map<String, Object> map = instanceToMap(
TestClass.TEST_VALUES,
builder.setFormatter(FieldNameFormatters.LOWER_UNDERSCORE).build()
);
assertThat(map.get("primBool"), nullValue());
assertThat(map.get("prim_bool"), is(true));
}
@Test
void instanceToMapContainsAllFields() {
assertThat(map.size(), is(34));
}
@Test
void instanceToMapDoesNotContainFinalStaticOrTransientFields() {
assertAll(
() -> assertThat(map.get("staticFinalInt"), is(nullValue())),
() -> assertThat(map.get("staticInt"), is(nullValue())),
() -> assertThat(map.get("finalInt"), is(nullValue())),
() -> assertThat(map.get("transientInt"), is(nullValue()))
);
}
@Test
void instanceToMapContainsAllSimpleFields() {
assertAll(
() -> assertThat(map.get("primBool"), is(t.getPrimBool())),
() -> assertThat(map.get("refBool"), is(t.getRefBool())),
() -> assertThat(map.get("primByte"), is(t.getPrimByte())),
() -> assertThat(map.get("refByte"), is(t.getRefByte())),
() -> assertThat(map.get("primChar"), is(t.getPrimChar())),
() -> assertThat(map.get("refChar"), is(t.getRefChar())),
() -> assertThat(map.get("primShort"), is(t.getPrimShort())),
() -> assertThat(map.get("refShort"), is(t.getRefShort())),
() -> assertThat(map.get("primInt"), is(t.getPrimInt())),
() -> assertThat(map.get("refInt"), is(t.getRefInt())),
() -> assertThat(map.get("primLong"), is(t.getPrimLong())),
() -> assertThat(map.get("refLong"), is(t.getRefLong())),
() -> assertThat(map.get("primFloat"), is(t.getPrimFloat())),
() -> assertThat(map.get("refFloat"), is(t.getRefFloat())),
() -> assertThat(map.get("primDouble"), is(t.getPrimDouble())),
() -> assertThat(map.get("refDouble"), is(t.getRefDouble())),
() -> assertThat(map.get("string"), is(t.getString()))
);
}
@Test
void instanceToMapContainsAllContainersOfSimpleTypes() {
assertAll(
() -> assertThat(map.get("ints"), is(t.getInts())),
() -> assertThat(map.get("strings"), is(t.getStrings())),
() -> assertThat(map.get("doubleByBool"), is(t.getDoubleByBool()))
);
}
@Test
void instanceToMapConvertsTypesToMaps() {
assertThat(map.get("subClass"), is(t.getSubClass().asMap()));
}
@Test
void instanceToMapConvertsExcludedClasses() {
assertThat(map.get("excludedClass"), is(t.getExcludedClass()));
}
@Test
void instanceToMapConvertsEnumsContainersToStringContainers() {
class A {
@ElementType(LocalTestEnum.class)
List<LocalTestEnum> el = listOf(LocalTestEnum.S, LocalTestEnum.T);
@ElementType(LocalTestEnum.class)
Set<LocalTestEnum> es = setOf(LocalTestEnum.S, LocalTestEnum.T);
@ElementType(LocalTestEnum.class)
Map<String, LocalTestEnum> em = mapOf(
"1", LocalTestEnum.S, "2", LocalTestEnum.T
);
}
Map<String, Object> map = instanceToMap(new A());
assertThat(map.get("el"), is(listOf("S", "T")));
assertThat(map.get("es"), is(setOf("S", "T")));
assertThat(map.get("em"), is(mapOf("1", "S", "2", "T")));
}
@Test
void instanceFromMapConvertsStringContainersToEnumContainers() {
class A {
@ElementType(LocalTestEnum.class)
List<LocalTestEnum> el = listOf();
@ElementType(LocalTestEnum.class)
Set<LocalTestEnum> es = setOf();
@ElementType(LocalTestEnum.class)
Map<String, LocalTestEnum> em = mapOf();
}
Map<String, Object> map = mapOf(
"el", listOf("S", "T"),
"es", setOf("S", "T"),
"em", mapOf("1", "S", "2", "T")
);
A a = instanceFromMap(new A(), map);
assertThat(a.el, is(listOf(LocalTestEnum.S, LocalTestEnum.T)));
assertThat(a.es, is(setOf(LocalTestEnum.S, LocalTestEnum.T)));
assertThat(a.em, is(mapOf(
"1", LocalTestEnum.S, "2", LocalTestEnum.T
)));
}
@Test
void instanceToMapConvertsContainerElementsToMaps() {
List<Map<String, Object>> subClassList = t.getSubClassList().stream()
.map(TestSubClass::asMap)
.collect(toList());
Set<Map<String, Object>> subClassSet = t.getSubClassSet().stream()
.map(TestSubClass::asMap)
.collect(toSet());
Map<String, Map<String, Object>> subClassMap = t.getSubClassMap()
.entrySet().stream()
.map(e -> mapEntry(e.getKey(), e.getValue().asMap()))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
assertAll(
() -> assertThat(map.get("subClassSet"), is(subClassSet)),
() -> assertThat(map.get("subClassMap"), is(subClassMap)),
() -> assertThat(map.get("subClassList"), is(subClassList))
);
}
@Test
void instanceToMapContainsNestedContainersOfSimpleTypes() {
assertAll(
() -> assertThat(map.get("listsList"), is(t.getListsList())),
() -> assertThat(map.get("setsSet"), is(t.getSetsSet())),
() -> assertThat(map.get("mapsMap"), is(t.getMapsMap()))
);
}
@Test
void instanceToMapContainsNestedContainersOfCustomTypes() {
List<List<Map<String, Object>>> lists = t.getSubClassListsList()
.stream().map(list -> list.stream()
.map(TestSubClass::asMap)
.collect(toList()))
.collect(toList());
Set<Set<Map<String, Object>>> sets = t.getSubClassSetsSet()
.stream().map(set -> set.stream()
.map(TestSubClass::asMap)
.collect(toSet()))
.collect(toSet());
Function<Map<String, TestSubClass>, Map<String, Map<String, Object>>> f =
map -> map.entrySet().stream().collect(
toMap(Map.Entry::getKey, e -> e.getValue().asMap())
);
Map<Integer, Map<String, Map<String, Object>>> m = t.getSubClassMapsMap()
.entrySet().stream()
.map(e -> mapEntry(e.getKey(), f.apply(e.getValue())))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
assertThat(map.get("subClassListsList"), is(lists));
assertThat(map.get("subClassSetsSet"), is(sets));
assertThat(map.get("subClassMapsMap"), is(m));
}
@Test
void instanceToMapContainsEnums() {
assertThat(map.get("e1"), is(t.getE1().toString()));
}
@Test
void instanceToMapContainsEnumLists() {
List<String> list = t.getEnums().stream()
.map(Enum::toString)
.collect(toList());
assertThat(map.get("enums"), is(list));
}
@Test
void instanceToMapConvertsConvertTypes() {
String s = new TestSubClassConverter()
.convertTo(t.getConverterSubClass(), null);
assertThat(map.get("converterSubClass"), is(s));
}
@Test
void instanceToMapCreatesLinkedHashMap() {
assertThat(instanceToMap(new Object()), instanceOf(LinkedHashMap.class));
}
private static Converter<Object, Object> converter = SIMPLE_TYPE_CONVERTER;
private static ConversionInfo newInfo(String fieldName, Object o) {
Field field = null;
try {
field = o.getClass().getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
MappingInfo mappingInfo = new MappingInfo(null, WITH_UPPER_FORMATTER);
return ConversionInfo.from(field, o, mappingInfo);
}
private static ConversionInfo newInfo(String fieldName) {
return newInfo(fieldName, new TestClass());
}
@Test
void typeConverterReturnsInstanceIfClassesMatch() {
//noinspection RedundantStringConstructorCall
String s = new String("123");
Object val = converter.convertFrom(s, newInfo("string"));
assertThat(val, sameInstance(s));
}
@Test
void typeConverterConvertsStringToCharacter() {
String s = "1";
Object vc = converter.convertFrom(s, newInfo("primChar"));
Object vd = converter.convertFrom(s, newInfo("refChar"));
assertThat(vc, instanceOf(Character.class));
assertThat(vd, instanceOf(Character.class));
}
@Test
void typeConverterChecksInvalidStrings() {
String msg = "An empty string cannot be converted to a character.";
assertThrows(
IllegalArgumentException.class,
() -> converter.convertFrom("", newInfo("refChar")),
msg
);
String value = "123";
msg = "String '" + value + "' is too long to be converted to a character";
assertThrows(
IllegalArgumentException.class,
() -> converter.convertFrom(value, newInfo("refChar")),
msg
);
}
@Test
void typeConverterConvertsNumbers() {
Number[] numbers = {
(byte) 1, (short) 2, 3, (long) 4,
(float) 5, (double) 6, 4L, 5f, 6d
};
String[] classes = {
"refByte", "refShort", "refInt",
"primLong", "refFloat", "refDouble"
};
for (String cls : classes) {
for (Number number : numbers) {
ConversionInfo info = newInfo(cls);
Object converted = converter.convertFrom(number, info);
assertThat(converted, instanceOf(info.getFieldType()));
}
}
}
@Test
void typeConverterChecksInvalidNumbers() {
String msg = "Number '1' cannot be converted to type 'String'";
assertThrows(
IllegalArgumentException.class,
() -> converter.convertFrom(1, newInfo("string")),
msg
);
}
private static final class ExcludedClass {}
@Test
void instanceToMapSetsAutoConvertedInstancesAsIs() {
class A {
@NoConvert
ExcludedClass ex = new ExcludedClass();
}
A a = new A();
Map<String, Object> map = instanceToMap(a);
assertThat(map.get("ex"), sameInstance(a.ex));
}
@Test
void instanceFromMapSetsAutoConvertedInstancesAsIs() {
class A {
@NoConvert
ExcludedClass ex = new ExcludedClass();
}
ExcludedClass cls = new ExcludedClass();
Map<String, Object> map = mapOf("ex", cls);
A a = instanceFromMap(new A(), map);
assertThat(a.ex, sameInstance(cls));
}
@Test
void fieldMapperUsesFiltersAdded() {
class A {
private int a = 1;
private int b = 2;
private int c = 3;
private transient int d = 4;
private final int e = 5;
}
Configuration.Properties props = Configuration.Properties.builder()
.addFilter(field -> !field.getName().equals("a"))
.addFilter(field -> !field.getName().equals("c"))
.build();
Map<String, Object> map = instanceToMap(new A(), props);
assertThat(map.size(), is(1));
assertThat(map, is(mapOf("b", 2)));
map = mapOf("a", -1, "b", -2, "c", -3, "d", -4, "e", -5);
A a = instanceFromMap(new A(), map, props);
assertThat(a.a, is(1));
assertThat(a.b, is(-2));
assertThat(a.c, is(3));
assertThat(a.d, is(4));
assertThat(a.e, is(5));
}
@Test
void selectFieldNameFormatterReturnsPropertiesIfNoFormatAnnotation() {
class A extends InSharedMemoryConfiguration {
protected A() { super(WITH_UPPER_FORMATTER); }
}
A a = new A();
FieldNameFormatter fnf = FieldMapper.selectFormatter(MappingInfo.from(a));
assertThat(fnf, sameInstance(FieldNameFormatters.UPPER_UNDERSCORE));
}
@Test
void selectFieldNameFormatterReturnsIfFormatAnnotation() {
@Format(FieldNameFormatters.LOWER_UNDERSCORE)
class A extends InSharedMemoryConfiguration {
protected A() { super(WITH_UPPER_FORMATTER); }
}
@Format(formatterClass = MyFormatter.class)
class B extends InSharedMemoryConfiguration {
protected B() { super(WITH_UPPER_FORMATTER); }
}
A a = new A();
FieldNameFormatter fnf = FieldMapper.selectFormatter(MappingInfo.from(a));
assertThat(fnf, sameInstance(FieldNameFormatters.LOWER_UNDERSCORE));
B b = new B();
fnf = FieldMapper.selectFormatter(MappingInfo.from(b));
assertThat(fnf, instanceOf(MyFormatter.class));
}
@Test
void formatterClassTakesPrecedence() {
@Format(
value = FieldNameFormatters.LOWER_UNDERSCORE,
formatterClass = MyFormatter.class
)
class A extends InSharedMemoryConfiguration {
protected A() { super(WITH_UPPER_FORMATTER); }
}
A a = new A();
FieldNameFormatter fnf = FieldMapper.selectFormatter(MappingInfo.from(a));
assertThat(fnf, instanceOf(MyFormatter.class));
}
private static final class MyFormatter implements FieldNameFormatter {
@Override
public String fromFieldName(String fieldName) {
return fieldName;
}
}
@Test
void fieldMapperUsesSelectFieldNameFormatter() {
@Format(FieldNameFormatters.LOWER_UNDERSCORE)
class A extends InSharedMemoryConfiguration {
private int abc = 1;
private int eFg = 2;
private int hIJ = 3;
protected A() { super(WITH_UPPER_FORMATTER); }
}
A a = new A();
MappingInfo mappingInfo = MappingInfo.from(a);
Map<String, Object> map = FieldMapper.instanceToMap(a, mappingInfo);
assertThat(map.get("abc"), is(1));
assertThat(map.get("e_fg"), is(2));
assertThat(map.get("h_i_j"), is(3));
map = mapOf("abc", 4, "e_fg", 5, "h_i_j", 6);
FieldMapper.instanceFromMap(a, map, mappingInfo);
assertThat(a.abc, is(4));
assertThat(a.eFg, is(5));
assertThat(a.hIJ, is(6));
}
}

@ -1,31 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.format.FieldNameFormatter;
import de.exlll.configlib.format.FieldNameFormatters;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class FieldNameFormattersTest {
@Test
void identityReturnsSameName() {
FieldNameFormatter formatter = FieldNameFormatters.IDENTITY;
assertThat(formatter.fromFieldName("fieldName"), is("fieldName"));
}
@Test
void lowerUnderscoreConvertsFromAndToCamelCase() {
FieldNameFormatter formatter = FieldNameFormatters.LOWER_UNDERSCORE;
assertThat(formatter.fromFieldName("fieldNameFormat"), is("field_name_format"));
}
@Test
void upperUnderscoreConvertsFromAndToCamelCase() {
FieldNameFormatter formatter = FieldNameFormatters.UPPER_UNDERSCORE;
assertThat(formatter.fromFieldName("fieldNameFormat"), is("FIELD_NAME_FORMAT"));
}
}

@ -1,61 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.classes.TestClass;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import static de.exlll.configlib.util.CollectionFactory.setOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class ReflectTest {
private static final Set<Class<?>> ALL_SIMPLE_TYPES = setOf(
boolean.class, Boolean.class,
byte.class, Byte.class,
char.class, Character.class,
short.class, Short.class,
int.class, Integer.class,
long.class, Long.class,
float.class, Float.class,
double.class, Double.class,
String.class
);
@Test
void isSimpleType() {
for (Class<?> cls : ALL_SIMPLE_TYPES) {
assertThat(Reflect.isSimpleType(cls), is(true));
}
assertThat(Reflect.isSimpleType(Object.class), is(false));
}
@Test
void isContainerType() {
assertThat(Reflect.isContainerType(Object.class), is(false));
assertThat(Reflect.isContainerType(HashMap.class), is(true));
assertThat(Reflect.isContainerType(HashSet.class), is(true));
assertThat(Reflect.isContainerType(ArrayList.class), is(true));
}
@Test
void getValue() throws NoSuchFieldException {
TestClass testClass = TestClass.TEST_VALUES;
Field f1 = TestClass.class.getDeclaredField("string");
Field f2 = TestClass.class.getDeclaredField("primLong");
Field f3 = TestClass.class.getDeclaredField("staticFinalInt");
Object value = Reflect.getValue(f1, testClass);
assertThat(value, is(testClass.getString()));
value = Reflect.getValue(f2, testClass);
assertThat(value, is(testClass.getPrimLong()));
value = Reflect.getValue(f3, testClass);
assertThat(value, is(TestClass.getStaticFinalInt()));
}
}

@ -1,667 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.ConfigurationElement;
import de.exlll.configlib.annotation.ElementType;
import de.exlll.configlib.classes.TestSubClass;
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;
import static de.exlll.configlib.FieldMapperHelpers.*;
import static de.exlll.configlib.util.CollectionFactory.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@SuppressWarnings("unused")
public class ValidatorTest {
@Test
void instanceFromMapRequiresMapToInitializeCustomClass() {
class A {
TestSubClass c = new TestSubClass();
}
Map<String, Object> map = mapOf(
"c", "s"
);
String msg = "Initializing field 'c' requires a Map<String, Object> " +
"but the given object is not a map." +
"\nType: 'String'\tValue: 's'";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapChecksEnumValuesAreString() {
class A {
LocalTestEnum t = LocalTestEnum.T;
}
Map<String, Object> map = mapOf(
"t", 1
);
String msg = "Initializing enum 't' requires a string but '1' is of " +
"type 'Integer'.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapRequiresMapWithStringsAsKeys() {
class A {
TestSubClass c = new TestSubClass();
}
Map<String, Object> map = mapOf(
"c", mapOf(1, 200, "string", "s")
);
String msg = "Initializing field 'c' requires a Map<String, Object> " +
"but the given map contains non-string keys." +
"\nAll entries: " + map.get("c");
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceToMapRequiresNonNullMapKeys() {
class A {
TestSubClass c = new TestSubClass();
}
Map<String, Object> m1 = new HashMap<>();
m1.put(null, "null");
Map<String, Object> m2 = mapOf("c", m1);
String msg = "Initializing field 'c' requires a Map<String, Object> " +
"but the given map contains non-string keys." +
"\nAll entries: {null=null}";
assertIfmCfgExceptionMessage(new A(), m2, msg);
}
@Test
void instanceFromMapRequiresCustomClassToHaveNoArgsConstructors() {
class A {
Sub3 s = new Sub3(1);
}
Map<String, Object> map = mapOf("s", mapOf());
String msg = "Type 'Sub3' of field 's' doesn't have a no-args constructor.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapRequiresCustomClassToBeConfigurationElements() {
class A {
Sub1 s = new Sub1();
}
Map<String, Object> map = mapOf("s", mapOf());
String msg = "Type 'Sub1' of field 's' is not annotated " +
"as a configuration element.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceFromMapChecksThatContainerTypesMatch() {
class A {
CopyOnWriteArrayList<?> l = new CopyOnWriteArrayList<>();
}
class B {
ConcurrentSkipListSet<?> s = new ConcurrentSkipListSet<>();
}
class C {
ConcurrentHashMap<?, ?> m = new ConcurrentHashMap<>();
}
Map<String, Object> m = mapOf("l", listOf("s"));
String msg = "Can not set field 'l' with type 'CopyOnWriteArrayList' " +
"to 'ArrayList'.";
assertIfmCfgExceptionMessage(new A(), m, msg);
m = mapOf("s", setOf("s"));
msg = "Can not set field 's' with type 'ConcurrentSkipListSet' " +
"to 'LinkedHashSet'.";
assertIfmCfgExceptionMessage(new B(), m, msg);
m = mapOf("m", mapOf(1, "s"));
msg = "Can not set field 'm' with type 'ConcurrentHashMap' " +
"to 'LinkedHashMap'.";
assertIfmCfgExceptionMessage(new C(), m, msg);
}
@Test
void instanceToMapThrowsExceptionIfDefaultValueIsNull() {
class A {
String string;
}
String msg = "The value of field 'string' is null.\n" +
"Please assign a non-null default value or remove this field.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceFromMapThrowsExceptionIfDefaultValueIsNull() {
class A {
String string;
}
Map<String, Object> map = mapOf("string", "s");
String msg = "The value of field 'string' is null.\n" +
"Please assign a non-null default value or remove this field.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceToMapRequiresListsWithoutElementTypeToContainSimpleTypes() {
class A {
List<TestSubClass> l = new ArrayList<>(listOf(
TestSubClass.TEST_VALUES
));
}
class B {
List<Set<Map<Integer, TestSubClass>>> l = new ArrayList<>(listOf(
setOf(mapOf(1, TestSubClass.TEST_VALUES))
));
}
String asString = TestSubClass.TEST_VALUES.toString();
A a = new A();
String msg = "The type of an element of list 'l' is not a simple type " +
"but list 'l' is missing the ElementType annotation.\n" +
"All elements: [" + asString + "]";
assertItmCfgExceptionMessage(a, msg);
B b = new B();
msg = "The type of an element of list 'l' is not a simple type " +
"but list 'l' is missing the ElementType annotation.\n" +
"All elements: [[{1=" + asString + "}]]";
assertItmCfgExceptionMessage(b, msg);
}
@Test
void instanceToMapRequiresSetsWithoutElementTypeToContainSimpleTypes() {
class A {
Set<TestSubClass> s = new HashSet<>(setOf(
TestSubClass.TEST_VALUES
));
}
class B {
Set<List<Map<Integer, TestSubClass>>> s = new HashSet<>(setOf(
listOf(mapOf(1, TestSubClass.TEST_VALUES))
));
}
String asString = TestSubClass.TEST_VALUES.toString();
A a = new A();
String msg = "The type of an element of set 's' is not a simple type " +
"but set 's' is missing the ElementType annotation.\n" +
"All elements: [" + asString + "]";
assertItmCfgExceptionMessage(a, msg);
B b = new B();
msg = "The type of an element of set 's' is not a simple type " +
"but set 's' is missing the ElementType annotation.\n" +
"All elements: [[{1=" + asString + "}]]";
assertItmCfgExceptionMessage(b, msg);
}
@Test
void instanceToMapRequiresMapsWithoutElementTypeToContainSimpleTypes() {
class A {
Map<Integer, TestSubClass> m = new HashMap<>(mapOf(
1, TestSubClass.TEST_VALUES
));
}
class B {
Map<Integer, Set<List<TestSubClass>>> m = new HashMap<>(mapOf(
1, setOf(listOf(TestSubClass.TEST_VALUES))
));
}
String asString = TestSubClass.TEST_VALUES.toString();
A a = new A();
String msg = "The type of a value of map 'm' is not a simple type " +
"but map 'm' is missing the ElementType annotation.\n" +
"All entries: {1=" + asString + "}";
assertItmCfgExceptionMessage(a, msg);
B b = new B();
msg = "The type of a value of map 'm' is not a simple type " +
"but map 'm' is missing the ElementType annotation.\n" +
"All entries: {1=[[" + asString + "]]}";
assertItmCfgExceptionMessage(b, msg);
}
@Test
void instanceToMapRequiresNonNullListElements() {
class A {
@ElementType(TestSubClass.class)
List<TestSubClass> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
}
A a = new A();
a.l1.add(null);
a.l2.add(null);
String msg = "An element of list 'l1' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [null]";
assertItmCfgExceptionMessage(a, msg);
a.l1.clear();
msg = "An element of list 'l2' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [null]";
assertItmCfgExceptionMessage(a, msg);
}
@Test
void instanceToMapRequiresNonNullListElementsRecursively() {
class A {
@ElementType(TestSubClass.class)
List<List<TestSubClass>> bla = new ArrayList<>();
}
A o = new A();
o.bla.add(new ArrayList<>());
o.bla.get(0).add(null);
String msg = "An element of list 'bla' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [[null]]";
assertItmCfgExceptionMessage(o, msg);
}
@Test
void instanceToMapRequiresNonNullSetElements() {
class A {
@ElementType(TestSubClass.class)
Set<TestSubClass> s1 = new HashSet<>();
Set<Integer> s2 = new HashSet<>();
}
A a = new A();
a.s1.add(null);
a.s2.add(null);
String msg = "An element of set 's1' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [null]";
assertItmCfgExceptionMessage(a, msg);
a.s1.clear();
msg = "An element of set 's2' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [null]";
assertItmCfgExceptionMessage(a, msg);
}
@Test
void instanceToMapRequiresNonNullSetElementsRecursively() {
class A {
@ElementType(TestSubClass.class)
Set<List<TestSubClass>> bla = new HashSet<>();
}
A o = new A();
o.bla.add(new ArrayList<>());
o.bla.iterator().next().add(null);
String msg = "An element of set 'bla' is null.\n" +
"Please either remove or replace this element.\n" +
"All elements: [[null]]";
assertItmCfgExceptionMessage(o, msg);
}
@Test
void instanceToMapRequiresNonNullMapValues() {
class A {
@ElementType(TestSubClass.class)
Map<Integer, TestSubClass> m1 = new HashMap<>();
Map<Integer, TestSubClass> m2 = new HashMap<>();
}
A a = new A();
a.m1.put(1, null);
a.m2.put(1, null);
String msg = "A value of map 'm1' is null.\n" +
"Please either remove or replace this element.\n" +
"All entries: {1=null}";
assertItmCfgExceptionMessage(a, msg);
a.m1.clear();
msg = "A value of map 'm2' is null.\n" +
"Please either remove or replace this element.\n" +
"All entries: {1=null}";
assertItmCfgExceptionMessage(a, msg);
}
@Test
void instanceToMapRequiresNonNullMapValuesRecursively() {
class A {
@ElementType(TestSubClass.class)
Map<Integer, List<TestSubClass>> bla = new HashMap<>();
}
A o = new A();
o.bla.put(1, new ArrayList<>());
o.bla.get(1).add(null);
String msg = "A value of map 'bla' is null.\n" +
"Please either remove or replace this element.\n" +
"All entries: {1=[null]}";
assertItmCfgExceptionMessage(o, msg);
}
@Test
void instanceToMapRequiresSimpleMapKeys() {
class A {
Map<TestSubClass, Integer> m = new HashMap<>();
}
A a = new A();
a.m.put(TestSubClass.TEST_VALUES, 1);
String msg = "The keys of map 'm' must be simple types.";
assertItmCfgExceptionMessage(a, msg);
}
@Test
void instanceToMapRequiresContainerTypesToMatchElementType() {
class A {
@ElementType(TestSubClass.class)
List<Integer> l = new ArrayList<>();
@ElementType(TestSubClass.class)
Set<Integer> s = new HashSet<>();
@ElementType(TestSubClass.class)
Map<Integer, Integer> m = new HashMap<>();
}
A a = new A();
a.l.add(1);
a.s.add(1);
a.m.put(1, 1);
String msg = "The type of an element of list 'l' doesn't match the " +
"type indicated by the ElementType annotation.\n" +
"Required type: 'TestSubClass'\tActual type: 'Integer'\n" +
"All elements: [1]";
assertItmCfgExceptionMessage(a, msg);
a.l.clear();
msg = "The type of an element of set 's' doesn't match the " +
"type indicated by the ElementType annotation.\n" +
"Required type: 'TestSubClass'\tActual type: 'Integer'\n" +
"All elements: [1]";
assertItmCfgExceptionMessage(a, msg);
a.s.clear();
msg = "The type of a value of map 'm' doesn't match the " +
"type indicated by the ElementType annotation.\n" +
"Required type: 'TestSubClass'\tActual type: 'Integer'\n" +
"All entries: {1=1}";
assertItmCfgExceptionMessage(a, msg);
}
@Test
void instanceToMapRequiresCustomClassesToBeConfigurationElements() {
class A {
Sub1 s = new Sub1();
}
class B {
Sub2 s = new Sub2();
}
Map<String, Object> map = mapOf("s", Collections.emptyMap());
assertThat(instanceToMap(new B()), is(map));
String msg = "Type 'Sub1' of field 's' is not annotated " +
"as a configuration element.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceToMapRequiresElementTypesToBeConcreteType() {
class A {
@ElementType(LocalTestInterface.class)
List<LocalTestInterface> l = new ArrayList<>();
}
class B {
@ElementType(LocalTestAbstractClass.class)
List<LocalTestAbstractClass> l = new ArrayList<>();
}
class C {
@ElementType(int.class)
List<LocalTestAbstractClass> l = new ArrayList<>();
}
class D {
@ElementType(TestSubClass[].class)
List<TestSubClass[]> l = new ArrayList<>();
}
class E {
@ElementType(LocalTestEnum.class)
List<LocalTestEnum> l = new ArrayList<>();
}
Map<String, Object> m = mapOf("l", listOf());
String msg = "The element type of field 'l' must be a concrete class " +
"but type 'LocalTestInterface' is an interface.";
assertItmCfgExceptionMessage(new A(), msg);
assertIfmCfgExceptionMessage(new A(), m, msg);
msg = "The element type of field 'l' must be a concrete class " +
"but type 'LocalTestAbstractClass' is an abstract class.";
assertItmCfgExceptionMessage(new B(), msg);
assertIfmCfgExceptionMessage(new B(), m, msg);
msg = "The element type 'int' of field 'l' is not a configuration element.";
assertItmCfgExceptionMessage(new C(), msg);
assertIfmCfgExceptionMessage(new C(), m, msg);
msg = "The element type 'TestSubClass[]' of field 'l' is " +
"not a configuration element.";
assertItmCfgExceptionMessage(new D(), msg);
assertIfmCfgExceptionMessage(new D(), m, msg);
}
@Test
void instanceToMapRequiresConfigurationElementsToHaveNoArgsConstructors() {
@ConfigurationElement
class Sub {
Sub(int n) {}
}
class A {
Sub s = new Sub(2);
}
String msg = "Type 'Sub' of field 's' doesn't have a no-args constructor.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceToMapRequiresElementTypesToBeConfigurationElements() {
class A {
@ElementType(String.class)
List<String> l = new ArrayList<>();
}
String msg = "The element type 'String' of field 'l' is not a " +
"configuration element.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceToMapRequiresElementTypesToHaveNoArgsConstructors() {
class A {
@ElementType(Sub3.class)
List<Sub3> list = new ArrayList<>();
}
String msg = "The element type 'Sub3' of field 'list' " +
"doesn't have a no-args constructor.";
assertItmCfgExceptionMessage(new A(), msg);
}
@Test
void instanceToAndFromMapRequireFieldsWithElementTypeToBeContainers() {
class A {
@ElementType(String.class)
String s = "";
}
String msg = "Field 's' is annotated with the ElementType annotation but " +
"is not a List, Set or Map.";
assertItmCfgExceptionMessage(new A(), msg);
assertIfmCfgExceptionMessage(new A(), mapOf("s", ""), msg);
}
@Test
void instanceFromMapsRequiresElementTypeToBeEnumType() {
class A {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
List<List<TestSubClass>> l = listOf();
}
Map<String, Object> map = mapOf(
"l", listOf(listOf("Q", "V"))
);
ConfigurationException ex = assertIfmThrowsCfgException(new A(), map);
Throwable cause = ex.getCause();
String msg = "Element type 'TestSubClass' of field 'l' is not an enum type.";
assertThat(cause.getMessage(), is(msg));
}
@Test
void instanceFromMapElementConverterRequiresObjectsOfTypeMapStringObject() {
class A {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
List<List<TestSubClass>> l = listOf();
}
Map<String, Object> map = mapOf(
"l", listOf(listOf(1, 2))
);
String msg = "Field 'l' of class 'A' has a nesting level of 1 but element " +
"'1' of type 'Integer' cannot be converted to 'TestSubClass'.";
assertIfmCfgExceptionMessage(new A(), map, msg);
}
@Test
void instanceToMapRequiresCorrectNestingLevelForLists() {
TestSubClass testValues = TestSubClass.TEST_VALUES;
class A {
@ElementType(TestSubClass.class)
List<List<TestSubClass>> l1 = listOf();
@ElementType(TestSubClass.class)
List<List<TestSubClass>> l2 = listOf(listOf(testValues));
}
class B {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
List<List<List<TestSubClass>>> l = listOf(listOf(listOf(testValues)));
}
class C {
@ElementType(value = TestSubClass.class, nestingLevel = 3)
List<List<List<TestSubClass>>> l = listOf(listOf(listOf(testValues)));
}
class D {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
List<List<TestSubClass>> l = listOf(listOf(
TestSubClass.of(11, "11"), TestSubClass.of(12, "12"),
TestSubClass.of(13, "13"), TestSubClass.of(14, "14")
));
}
String msg = "Field 'l2' of class 'A' has a nesting level of 0 but the " +
"first object of type 'TestSubClass' was found on level 1.";
assertItmCfgExceptionMessage(new A(), msg);
msg = "Field 'l' of class 'B' has a nesting level of 1 but the " +
"first object of type 'TestSubClass' was found on level 2.";
assertItmCfgExceptionMessage(new B(), msg);
msg = "Field 'l' of class 'C' has a nesting level of 3 but the " +
"first object of type 'TestSubClass' was found on level 2.";
assertItmCfgExceptionMessage(new C(), msg);
Map<String, Object> map = instanceToMap(new D());
D d = instanceFromMap(new D(), map);
assertThat(d.l, is(new D().l));
}
@Test
void instanceToMapRequiresCorrectNestingLevelForMaps() {
TestSubClass testValues = TestSubClass.TEST_VALUES;
class A {
@ElementType(TestSubClass.class)
Map<String, Map<String, TestSubClass>> m1 = mapOf();
@ElementType(TestSubClass.class)
Map<Integer, Map<String, TestSubClass>> m2 = mapOf(
1, mapOf("1", TestSubClass.of(11, "11")),
2, mapOf("2", TestSubClass.of(12, "12"))
);
}
class B {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
Map<String, Map<String, Map<String, TestSubClass>>> m = mapOf(
"1", mapOf("2", mapOf("3", testValues)),
"1", mapOf("2", mapOf("3", testValues))
);
}
class C {
@ElementType(value = TestSubClass.class, nestingLevel = 3)
Map<String, Map<String, Map<String, TestSubClass>>> m = mapOf(
"1", mapOf("2", mapOf("3", testValues)),
"1", mapOf("2", mapOf("3", testValues))
);
}
class D {
@ElementType(value = TestSubClass.class, nestingLevel = 1)
Map<Integer, Map<String, TestSubClass>> m = mapOf(
1, mapOf("1", TestSubClass.of(11, "11")),
2, mapOf("2", TestSubClass.of(12, "12")),
3, mapOf("3", TestSubClass.of(13, "13")),
4, mapOf("4", TestSubClass.of(14, "14"))
);
}
String msg = "Field 'm2' of class 'A' has a nesting level of 0 but the " +
"first object of type 'TestSubClass' was found on level 1.";
assertItmCfgExceptionMessage(new A(), msg);
msg = "Field 'm' of class 'B' has a nesting level of 1 but the " +
"first object of type 'TestSubClass' was found on level 2.";
assertItmCfgExceptionMessage(new B(), msg);
msg = "Field 'm' of class 'C' has a nesting level of 3 but the " +
"first object of type 'TestSubClass' was found on level 2.";
assertItmCfgExceptionMessage(new C(), msg);
Map<String, Object> map = instanceToMap(new D());
D d = instanceFromMap(new D(), map);
assertThat(d.m, is(new D().m));
}
/* The case that the nestingLevel is set to high cannot properly be detected. */
@Test
void instanceFromMapRequiresCorrectNestingLevelForLists() {
class A {
@ElementType(TestSubClass.class)
List<TestSubClass> l = listOf();
}
class B {
@ElementType(LocalTestEnum.class)
List<LocalTestEnum> l = listOf();
}
class C {
@ElementType(TestSubClass.class)
List<List<TestSubClass>> l = listOf();
}
class D {
@ElementType(LocalTestEnum.class)
List<List<LocalTestEnum>> l = listOf();
}
Map<String, Object> m = TestSubClass.TEST_VALUES.asMap();
instanceFromMap(new A(), mapOf("l", listOf(m)));
instanceFromMap(new B(), mapOf("l", listOf("S", "T")));
String elementAsString = m.toString();
String msg = "Field 'l' of class 'C' has a nesting level of 0 but element '[" +
elementAsString + "]' of type 'ArrayList' cannot be converted " +
"to 'TestSubClass'.";
assertIfmCfgExceptionMessage(new C(), mapOf("l", listOf(listOf(m))), msg);
msg = "Field 'l' of class 'D' has a nesting level of 0 but element '[S, T]' of type " +
"'ArrayList' cannot be converted to 'LocalTestEnum'.";
assertIfmCfgExceptionMessage(new D(), mapOf("l", listOf(listOf("S", "T"))), msg);
}
}

@ -1,7 +0,0 @@
package de.exlll.configlib.classes;
public final class ClassWithFinalStaticTransientField {
private final int i = 0;
private static int j;
private transient int k;
}

@ -1,482 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.annotation.Comment;
import de.exlll.configlib.annotation.Convert;
import de.exlll.configlib.annotation.ElementType;
import de.exlll.configlib.annotation.NoConvert;
import de.exlll.configlib.configs.yaml.YamlConfiguration;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import static de.exlll.configlib.util.CollectionFactory.*;
@SuppressWarnings("FieldCanBeLocal")
@Comment({"A", "", "B", "C"})
public final class TestClass extends YamlConfiguration {
public static final TestClass TEST_VALUES;
public enum TestEnum {
DEFAULT, NON_DEFAULT
}
static {
TEST_VALUES = new TestClass();
TEST_VALUES.primBool = true;
TEST_VALUES.refBool = true;
TEST_VALUES.primByte = 1;
TEST_VALUES.refByte = 2;
TEST_VALUES.primChar = 'c';
TEST_VALUES.refChar = 'd';
TEST_VALUES.primShort = 3;
TEST_VALUES.refShort = 4;
TEST_VALUES.primInt = 5;
TEST_VALUES.refInt = 6;
TEST_VALUES.primLong = 7;
TEST_VALUES.refLong = 8L;
TEST_VALUES.primFloat = 9.0f;
TEST_VALUES.refFloat = 10.0f;
TEST_VALUES.primDouble = 11.0;
TEST_VALUES.refDouble = 12.0;
TEST_VALUES.string = "string";
/* other types */
TEST_VALUES.subClass = TestSubClass.TEST_VALUES;
/* containers of simple types */
TEST_VALUES.ints = setOf(1, 2, 3);
TEST_VALUES.strings = listOf("a", "b", "c");
TEST_VALUES.doubleByBool = mapOf(true, 1.0, false, 2.0);
/* containers of other types */
TEST_VALUES.subClassSet = setOf(
TestSubClass.of(1, "1"),
TestSubClass.of(2, "2")
);
TEST_VALUES.subClassList = listOf(
TestSubClass.of(3, "3"),
TestSubClass.of(4, "4")
);
TEST_VALUES.subClassMap = mapOf(
"5", TestSubClass.of(5, "5"),
"6", TestSubClass.of(6, "6")
);
/* nested containers of simple types */
TEST_VALUES.listsList = listOf(
listOf(1, 2), listOf(3, 4)
);
TEST_VALUES.setsSet = setOf(
setOf("a", "b"), setOf("c", "d")
);
TEST_VALUES.mapsMap = mapOf(
1, mapOf("1", 1), 2, mapOf("2", 2)
);
/* nested containers of custom types */
TEST_VALUES.subClassListsList = listOf(
listOf(TestSubClass.of(7, "7"), TestSubClass.of(8, "8"))
);
TEST_VALUES.subClassSetsSet = setOf(setOf(
TestSubClass.of(9, "9"), TestSubClass.of(10, "10")
));
TEST_VALUES.subClassMapsMap = mapOf(
1, mapOf("1", TestSubClass.of(11, "11")),
2, mapOf("2", TestSubClass.of(12, "12"))
);
TEST_VALUES.e1 = TestEnum.NON_DEFAULT;
TEST_VALUES.enums = listOf(TestEnum.DEFAULT, TestEnum.NON_DEFAULT);
TEST_VALUES.converterSubClass = TestSubClass.of(13, "13");
TEST_VALUES.excludedClass = TestExcludedClass.TEST_VALUES;
}
/* not converted */
private static final int staticFinalInt = 1;
private static int staticInt = 2;
private final int finalInt = 3;
private transient int transientInt = 4;
/* simple types */
@Comment({"A"})
private boolean primBool;
@Comment({"B", "C"})
private Boolean refBool = false;
@Comment({"D", "", "E"})
private byte primByte;
private Byte refByte = 0;
@Comment("F")
private char primChar;
@Comment({"", "G"})
private Character refChar = '\0';
private short primShort;
private Short refShort = 0;
private int primInt;
private Integer refInt = 0;
private long primLong;
private Long refLong = 0L;
private float primFloat;
private Float refFloat = 0F;
private double primDouble;
private Double refDouble = 0.0;
private String string = "";
/* other types */
private TestSubClass subClass = new TestSubClass();
/* containers of simple types */
private Set<Integer> ints = new HashSet<>();
private List<String> strings = new ArrayList<>();
private Map<Boolean, Double> doubleByBool = new HashMap<>();
/* containers of other types */
@ElementType(TestSubClass.class)
private Set<TestSubClass> subClassSet = new HashSet<>();
@ElementType(TestSubClass.class)
private List<TestSubClass> subClassList = new ArrayList<>();
@ElementType(TestSubClass.class)
private Map<String, TestSubClass> subClassMap = new HashMap<>();
/* nested containers of simple types */
private List<List<Integer>> listsList = new ArrayList<>();
private Set<Set<String>> setsSet = new HashSet<>();
private Map<Integer, Map<String, Integer>> mapsMap = new HashMap<>();
/* nested containers of custom types */
@ElementType(value = TestSubClass.class, nestingLevel = 1)
private List<List<TestSubClass>> subClassListsList = new ArrayList<>();
@ElementType(value = TestSubClass.class, nestingLevel = 1)
private Set<Set<TestSubClass>> subClassSetsSet = new HashSet<>();
@ElementType(value = TestSubClass.class, nestingLevel = 1)
private Map<Integer, Map<String, TestSubClass>> subClassMapsMap
= new HashMap<>();
private TestEnum e1 = TestEnum.DEFAULT;
@ElementType(TestEnum.class)
private List<TestEnum> enums = new ArrayList<>();
@Convert(TestSubClassConverter.class)
private TestSubClass converterSubClass = new TestSubClass();
@NoConvert
private TestExcludedClass excludedClass = new TestExcludedClass();
public TestClass(Path path, YamlProperties properties) {
super(path, properties);
}
public TestClass(Path path) {
super(path);
}
public TestClass() {
this(Paths.get(""), YamlProperties.DEFAULT);
}
public TestClass(Path configPath, TestClass other) {
this(configPath);
this.transientInt = other.transientInt;
this.primBool = other.primBool;
this.refBool = other.refBool;
this.primByte = other.primByte;
this.refByte = other.refByte;
this.primChar = other.primChar;
this.refChar = other.refChar;
this.primShort = other.primShort;
this.refShort = other.refShort;
this.primInt = other.primInt;
this.refInt = other.refInt;
this.primLong = other.primLong;
this.refLong = other.refLong;
this.primFloat = other.primFloat;
this.refFloat = other.refFloat;
this.primDouble = other.primDouble;
this.refDouble = other.refDouble;
this.string = other.string;
this.subClass = other.subClass;
this.ints = other.ints;
this.strings = other.strings;
this.doubleByBool = other.doubleByBool;
this.subClassSet = other.subClassSet;
this.subClassList = other.subClassList;
this.subClassMap = other.subClassMap;
this.listsList = other.listsList;
this.setsSet = other.setsSet;
this.mapsMap = other.mapsMap;
this.subClassListsList = other.subClassListsList;
this.subClassSetsSet = other.subClassSetsSet;
this.subClassMapsMap = other.subClassMapsMap;
this.e1 = other.e1;
this.enums = other.enums;
this.converterSubClass = other.converterSubClass;
this.excludedClass = other.excludedClass;
}
public static int getStaticFinalInt() {
return staticFinalInt;
}
public static int getStaticInt() {
return staticInt;
}
public int getFinalInt() {
return finalInt;
}
public int getTransientInt() {
return transientInt;
}
public boolean getPrimBool() {
return primBool;
}
public Boolean getRefBool() {
return refBool;
}
public byte getPrimByte() {
return primByte;
}
public Byte getRefByte() {
return refByte;
}
public char getPrimChar() {
return primChar;
}
public Character getRefChar() {
return refChar;
}
public short getPrimShort() {
return primShort;
}
public Short getRefShort() {
return refShort;
}
public int getPrimInt() {
return primInt;
}
public Integer getRefInt() {
return refInt;
}
public long getPrimLong() {
return primLong;
}
public Long getRefLong() {
return refLong;
}
public float getPrimFloat() {
return primFloat;
}
public Float getRefFloat() {
return refFloat;
}
public double getPrimDouble() {
return primDouble;
}
public Double getRefDouble() {
return refDouble;
}
public String getString() {
return string;
}
public TestSubClass getSubClass() {
return subClass;
}
public Set<Integer> getInts() {
return ints;
}
public List<String> getStrings() {
return strings;
}
public Map<Boolean, Double> getDoubleByBool() {
return doubleByBool;
}
public Set<TestSubClass> getSubClassSet() {
return subClassSet;
}
public List<TestSubClass> getSubClassList() {
return subClassList;
}
public Map<String, TestSubClass> getSubClassMap() {
return subClassMap;
}
public List<List<Integer>> getListsList() {
return listsList;
}
public Set<Set<String>> getSetsSet() {
return setsSet;
}
public Map<Integer, Map<String, Integer>> getMapsMap() {
return mapsMap;
}
public List<List<TestSubClass>> getSubClassListsList() {
return subClassListsList;
}
public Set<Set<TestSubClass>> getSubClassSetsSet() {
return subClassSetsSet;
}
public Map<Integer, Map<String, TestSubClass>> getSubClassMapsMap() {
return subClassMapsMap;
}
public TestEnum getE1() {
return e1;
}
public List<TestEnum> getEnums() {
return enums;
}
public TestSubClass getConverterSubClass() {
return converterSubClass;
}
public TestExcludedClass getExcludedClass() {
return excludedClass;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TestClass)) return false;
TestClass testClass = (TestClass) o;
if (finalInt != testClass.finalInt) return false;
if (transientInt != testClass.transientInt) return false;
if (primBool != testClass.primBool) return false;
if (primByte != testClass.primByte) return false;
if (primChar != testClass.primChar) return false;
if (primShort != testClass.primShort) return false;
if (primInt != testClass.primInt) return false;
if (primLong != testClass.primLong) return false;
if (Float.compare(testClass.primFloat, primFloat) != 0) return false;
if (Double.compare(testClass.primDouble, primDouble) != 0) return false;
if (!refBool.equals(testClass.refBool)) return false;
if (!refByte.equals(testClass.refByte)) return false;
if (!refChar.equals(testClass.refChar)) return false;
if (!refShort.equals(testClass.refShort)) return false;
if (!refInt.equals(testClass.refInt)) return false;
if (!refLong.equals(testClass.refLong)) return false;
if (!refFloat.equals(testClass.refFloat)) return false;
if (!refDouble.equals(testClass.refDouble)) return false;
if (!string.equals(testClass.string)) return false;
if (!subClass.equals(testClass.subClass)) return false;
if (!ints.equals(testClass.ints)) return false;
if (!strings.equals(testClass.strings)) return false;
if (!doubleByBool.equals(testClass.doubleByBool)) return false;
if (!subClassSet.equals(testClass.subClassSet)) return false;
if (!subClassList.equals(testClass.subClassList)) return false;
if (!subClassMap.equals(testClass.subClassMap)) return false;
if (!listsList.equals(testClass.listsList)) return false;
if (!setsSet.equals(testClass.setsSet)) return false;
if (!mapsMap.equals(testClass.mapsMap)) return false;
if (!subClassListsList.equals(testClass.subClassListsList)) return false;
if (!subClassSetsSet.equals(testClass.subClassSetsSet)) return false;
if (e1 != testClass.e1) return false;
if (!enums.equals(testClass.enums)) return false;
if (!converterSubClass.equals(testClass.converterSubClass)) return false;
if (!excludedClass.equals(testClass.excludedClass)) return false;
return subClassMapsMap.equals(testClass.subClassMapsMap);
}
@Override
public int hashCode() {
int result;
long temp;
result = finalInt;
result = 31 * result + transientInt;
result = 31 * result + (primBool ? 1 : 0);
result = 31 * result + refBool.hashCode();
result = 31 * result + (int) primByte;
result = 31 * result + refByte.hashCode();
result = 31 * result + (int) primChar;
result = 31 * result + refChar.hashCode();
result = 31 * result + (int) primShort;
result = 31 * result + refShort.hashCode();
result = 31 * result + primInt;
result = 31 * result + refInt.hashCode();
result = 31 * result + (int) (primLong ^ (primLong >>> 32));
result = 31 * result + refLong.hashCode();
result = 31 * result + (primFloat != +0.0f ? Float.floatToIntBits(
primFloat) : 0);
result = 31 * result + refFloat.hashCode();
temp = Double.doubleToLongBits(primDouble);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + refDouble.hashCode();
result = 31 * result + string.hashCode();
result = 31 * result + subClass.hashCode();
result = 31 * result + ints.hashCode();
result = 31 * result + strings.hashCode();
result = 31 * result + doubleByBool.hashCode();
result = 31 * result + subClassSet.hashCode();
result = 31 * result + subClassList.hashCode();
result = 31 * result + subClassMap.hashCode();
result = 31 * result + listsList.hashCode();
result = 31 * result + setsSet.hashCode();
result = 31 * result + mapsMap.hashCode();
result = 31 * result + subClassListsList.hashCode();
result = 31 * result + subClassSetsSet.hashCode();
result = 31 * result + subClassMapsMap.hashCode();
result = 31 * result + e1.hashCode();
result = 31 * result + enums.hashCode();
result = 31 * result + converterSubClass.hashCode();
result = 31 * result + excludedClass.hashCode();
return result;
}
@Override
public String toString() {
return "TestClass{" +
"\nprimBool=" + primBool +
",\nrefBool=" + refBool +
",\nprimByte=" + primByte +
",\nrefByte=" + refByte +
",\nprimChar=" + primChar +
",\nrefChar=" + refChar +
",\nprimShort=" + primShort +
",\nrefShort=" + refShort +
",\nprimInt=" + primInt +
",\nrefInt=" + refInt +
",\nprimLong=" + primLong +
",\nrefLong=" + refLong +
",\nprimFloat=" + primFloat +
",\nrefFloat=" + refFloat +
",\nprimDouble=" + primDouble +
",\nrefDouble=" + refDouble +
",\nstring='" + string + '\'' +
",\nsubClass=" + subClass +
",\nints=" + ints +
",\nstrings=" + strings +
",\ndoubleByBool=" + doubleByBool +
",\nsubClassSet=" + subClassSet +
",\nsubClassList=" + subClassList +
",\nsubClassMap=" + subClassMap +
",\nlistsList=" + listsList +
",\nsetsSet=" + setsSet +
",\nmapsMap=" + mapsMap +
",\nsubClassListsList=" + subClassListsList +
",\nsubClassSetsSet=" + subClassSetsSet +
",\nsubClassMapsMap=" + subClassMapsMap +
",\ne1=" + e1 +
",\nenums=" + enums +
",\nconverterSubClass=" + converterSubClass +
",\nexcludedClass=" + excludedClass +
'}';
}
}

@ -1,55 +0,0 @@
package de.exlll.configlib.classes;
public class TestExcludedClass {
public static final TestExcludedClass TEST_VALUES;
private int primInt;
private String string = "";
static {
TEST_VALUES = new TestExcludedClass();
TEST_VALUES.primInt = 1;
TEST_VALUES.string = "string";
}
public int getPrimInt() {
return primInt;
}
public String getString() {
return string;
}
public void setPrimInt(int primInt) {
this.primInt = primInt;
}
public void setString(String string) {
this.string = string;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestExcludedClass that = (TestExcludedClass) o;
if (primInt != that.primInt) return false;
return string.equals(that.string);
}
@Override
public int hashCode() {
int result = primInt;
result = 31 * result + string.hashCode();
return result;
}
@Override
public String toString() {
return "TestExcludedClass{" +
"primInt=" + primInt +
", string='" + string + '\'' +
'}';
}
}

@ -1,149 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.annotation.ConfigurationElement;
import de.exlll.configlib.annotation.ElementType;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static de.exlll.configlib.util.CollectionFactory.*;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
@SuppressWarnings("FieldCanBeLocal")
@ConfigurationElement
public final class TestSubClass {
public static final TestSubClass TEST_VALUES;
static {
TEST_VALUES = new TestSubClass();
TEST_VALUES.primInt = 1;
TEST_VALUES.string = "string";
TEST_VALUES.list = listOf("list");
TEST_VALUES.set = setOf("set");
TEST_VALUES.map = mapOf("map", 1);
TEST_VALUES.testSubSubClass = TestSubSubClass.of(14, "14");
TEST_VALUES.subClassList = listOf(
TestSubSubClass.of(15, "15"), TestSubSubClass.of(16, "16")
);
TEST_VALUES.subClassSet = setOf(
TestSubSubClass.of(17, "17"), TestSubSubClass.of(18, "18")
);
TEST_VALUES.subClassMap = mapOf("map", TestSubSubClass.of(19, "19"));
}
private final int finalInt = 1;
private int primInt;
private String string = "";
private List<String> list = Collections.emptyList();
private Set<String> set = Collections.emptySet();
private Map<String, Integer> map = Collections.emptyMap();
private TestSubSubClass testSubSubClass = new TestSubSubClass();
@ElementType(TestSubSubClass.class)
private List<TestSubSubClass> subClassList = Collections.emptyList();
@ElementType(TestSubSubClass.class)
private Set<TestSubSubClass> subClassSet = Collections.emptySet();
@ElementType(TestSubSubClass.class)
private Map<String, TestSubSubClass> subClassMap = Collections.emptyMap();
public static TestSubClass of(int primInt, String string) {
TestSubClass cls = new TestSubClass();
cls.primInt = primInt;
cls.string = string;
cls.list = listOf(string);
cls.set = setOf(string);
cls.map = mapOf(string, primInt);
cls.testSubSubClass = TestSubSubClass.of(primInt, string);
cls.subClassList = listOf(
TestSubSubClass.of(primInt * 100, string)
);
cls.subClassSet = setOf(
TestSubSubClass.of(primInt * 101, string)
);
cls.subClassMap = mapOf(string, TestSubSubClass.of(primInt * 102, string));
return cls;
}
public Map<String, Object> asMap() {
Map<String, Object> asMap = mapOf("primInt", primInt, "string", string);
asMap.put("list", list);
asMap.put("set", set);
asMap.put("map", map);
asMap.put("testSubSubClass", testSubSubClass.asMap());
asMap.put(
"subClassList", subClassList.stream()
.map(TestSubSubClass::asMap)
.collect(toList())
);
asMap.put(
"subClassSet", subClassSet.stream()
.map(TestSubSubClass::asMap)
.collect(toSet())
);
asMap.put(
"subClassMap", subClassMap.entrySet().stream()
.map(e -> mapEntry(e.getKey(), e.getValue().asMap()))
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue))
);
return asMap;
}
public int getPrimInt() {
return primInt;
}
public String getString() {
return string;
}
@Override
public String toString() {
return "TestSubClass{" +
"primInt=" + primInt +
", string='" + string + '\'' +
", list=" + list +
", set=" + set +
", map=" + map +
", testSubSubClass=" + testSubSubClass +
", subClassList=" + subClassList +
", subClassSet=" + subClassSet +
", subClassMap=" + subClassMap +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestSubClass that = (TestSubClass) o;
if (primInt != that.primInt) return false;
if (!string.equals(that.string)) return false;
if (!list.equals(that.list)) return false;
if (!set.equals(that.set)) return false;
if (!map.equals(that.map)) return false;
if (!testSubSubClass.equals(that.testSubSubClass)) return false;
if (!subClassList.equals(that.subClassList)) return false;
if (!subClassSet.equals(that.subClassSet)) return false;
return subClassMap.equals(that.subClassMap);
}
@Override
public int hashCode() {
int result = finalInt;
result = 31 * result + primInt;
result = 31 * result + string.hashCode();
result = 31 * result + list.hashCode();
result = 31 * result + set.hashCode();
result = 31 * result + map.hashCode();
result = 31 * result + testSubSubClass.hashCode();
result = 31 * result + subClassList.hashCode();
result = 31 * result + subClassSet.hashCode();
result = 31 * result + subClassMap.hashCode();
return result;
}
}

@ -1,23 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.Converter;
public final class TestSubClassConverter
implements Converter<TestSubClass, String> {
@Override
public String convertTo(TestSubClass element, ConversionInfo info) {
return element.getPrimInt() + ":" + element.getString();
}
@Override
public TestSubClass convertFrom(String element, ConversionInfo info) {
String[] split = element.split(":");
return TestSubClass.of(Integer.parseInt(split[0]), split[1]);
}
@Override
public String toString() {
return "TestSubClassConverter";
}
}

@ -1,83 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.annotation.ConfigurationElement;
import java.util.*;
import static de.exlll.configlib.util.CollectionFactory.listOf;
import static de.exlll.configlib.util.CollectionFactory.mapOf;
import static de.exlll.configlib.util.CollectionFactory.setOf;
@ConfigurationElement
public final class TestSubSubClass {
public static final TestSubSubClass TEST_VALUES;
static {
TEST_VALUES = new TestSubSubClass();
TEST_VALUES.primInt = 1;
TEST_VALUES.string = "string";
TEST_VALUES.list = listOf("list");
TEST_VALUES.set = setOf("set");
TEST_VALUES.map = mapOf("map", 1);
}
private int primInt;
private String string = "";
private List<String> list = listOf();
private Set<String> set = setOf();
private Map<String, Integer> map = mapOf();
public static TestSubSubClass of(int primInt, String string) {
TestSubSubClass cls = new TestSubSubClass();
cls.primInt = primInt;
cls.string = string;
String concat = string + string;
cls.list = listOf(concat);
cls.set = setOf(concat);
cls.map = mapOf(concat, primInt);
return cls;
}
public Map<String, Object> asMap() {
Map<String, Object> asMap = mapOf("primInt", primInt, "string", string);
asMap.put("list", list);
asMap.put("set", set);
asMap.put("map", map);
return asMap;
}
@Override
public String toString() {
return "TestSubSubClass{" +
"primInt=" + primInt +
", string='" + string + '\'' +
", list=" + list +
", set=" + set +
", map=" + map +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestSubSubClass that = (TestSubSubClass) o;
if (primInt != that.primInt) return false;
if (!string.equals(that.string)) return false;
if (!list.equals(that.list)) return false;
if (!set.equals(that.set)) return false;
return map.equals(that.map);
}
@Override
public int hashCode() {
int result = primInt;
result = 31 * result + string.hashCode();
result = 31 * result + list.hashCode();
result = 31 * result + set.hashCode();
result = 31 * result + map.hashCode();
return result;
}
}

@ -1,45 +0,0 @@
package de.exlll.configlib.configs.mem;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.ConfigurationSource;
import java.util.Map;
import java.util.Objects;
public class InSharedMemoryConfiguration
extends Configuration<InSharedMemoryConfiguration> {
private final InSharedMemorySource source = new InSharedMemorySource();
protected InSharedMemoryConfiguration(Properties properties) {
super(properties);
}
@Override
protected ConfigurationSource<InSharedMemoryConfiguration> getSource() {
return source;
}
@Override
protected InSharedMemoryConfiguration getThis() {
return this;
}
private static final class InSharedMemorySource implements
ConfigurationSource<InSharedMemoryConfiguration> {
private static Map<String, Object> map;
@Override
public Map<String, Object> loadConfiguration(
InSharedMemoryConfiguration config
) {
return Objects.requireNonNull(map);
}
@Override
public void saveConfiguration(
InSharedMemoryConfiguration config, Map<String, Object> map
) {
InSharedMemorySource.map = map;
}
}
}

@ -1,941 +0,0 @@
package de.exlll.configlib.configs.yaml;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.annotation.Comment;
import de.exlll.configlib.classes.TestClass;
import de.exlll.configlib.configs.yaml.YamlConfiguration.YamlProperties;
import de.exlll.configlib.format.FieldNameFormatters;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static de.exlll.configlib.util.CollectionFactory.listOf;
import static java.util.stream.Collectors.joining;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SuppressWarnings("unused")
class YamlConfigurationTest {
private FileSystem fileSystem;
private Path testPath, configPath;
@BeforeEach
void setUp() {
fileSystem = Jimfs.newFileSystem();
testPath = fileSystem.getPath("/a/b/test.yml");
configPath = fileSystem.getPath("/a/b/config.yml");
}
@AfterEach
void tearDown() throws IOException {
fileSystem.close();
}
@Test
void loadAndSaveExecutesPostLoadHook() {
class A extends YamlConfiguration {
int i = 0;
protected A() { super(configPath, YamlProperties.DEFAULT); }
@Override
protected void postLoad() {
i++;
}
}
A a = new A();
a.loadAndSave();
assertThat(a.i, is(1));
}
@Test
void loadAndSaveSavesConfiguration() {
YamlConfiguration configuration = new TestClass(
configPath, TestClass.TEST_VALUES
);
configuration.loadAndSave();
assertThat(Files.exists(configPath), is(true));
YamlConfiguration load = new TestClass(configPath);
load.load();
assertThat(load, is(TestClass.TEST_VALUES));
}
@Test
void loadAndSaveLoadsConfiguration() {
new TestClass(configPath, TestClass.TEST_VALUES).save();
YamlConfiguration configuration = new TestClass(configPath);
configuration.loadAndSave();
assertThat(configuration, is(TestClass.TEST_VALUES));
assertThat(Files.exists(configPath), is(true));
}
@Test
void loadLoadsConfig() {
setupConfigPath();
Configuration configuration = new TestClass(configPath);
assertThat(configuration, is(not(TestClass.TEST_VALUES)));
configuration.load();
assertThat(configuration, is((TestClass.TEST_VALUES)));
}
private void setupConfigPath() {
Configuration configuration = new TestClass(
configPath, TestClass.TEST_VALUES
);
configuration.save();
}
@Test
void loadThrowsExceptionIfTypesDoNotMatch() {
Configuration configuration = new TestClass(configPath);
configuration.save();
assertThrows(IllegalArgumentException.class, configuration::load);
}
@Test
void saveCreatesConfig() {
assertThat(Files.exists(testPath), is(false));
Configuration configuration = new TestClass(testPath);
configuration.save();
assertThat(Files.exists(testPath), is(true));
}
@Test
void saveDumpsYaml() throws IOException {
Configuration configuration = new TestClass(
testPath, TestClass.TEST_VALUES
);
configuration.save();
assertThat(readConfig(testPath), is(TEST_CLASS_YML));
}
@Test
void saveDumpsPrependedAndAppendedComments() throws IOException {
class A extends YamlConfiguration {
int i;
protected A(YamlProperties properties) {
super(testPath, properties);
}
}
YamlProperties properties = YamlProperties.builder()
.setPrependedComments(listOf("AB", "", "CD"))
.setAppendedComments(listOf("AB", "", "CD"))
.build();
new A(properties).save();
assertThat(readConfig(testPath), is(PRE_AND_APPENDED_COMMENTS_YML));
}
@Test
void saveDumpsClassComments() throws IOException {
@Comment({"1", "", "2"})
class A extends YamlConfiguration {
@Comment("a")
private int a = 1;
@Comment({"b", "x"})
private int b = 2;
@Comment({"c", "", "y"})
private int c = 3;
private int d = 4;
protected A() { super(testPath); }
}
new A().save();
assertThat(readConfig(testPath), is(CLASS_COMMENTS_YML));
}
@Test
void saveDumpsFieldComments() throws IOException {
class A extends YamlConfiguration {
@Comment("a")
private int a = 1;
@Comment({"b", "x"})
private int b = 2;
@Comment({"c", "", "y"})
private int c = 3;
private int d = 4;
protected A() { super(testPath); }
}
new A().save();
assertThat(readConfig(testPath), is(FIELD_COMMENTS_YML));
}
@Test
void saveDumpsFormattedFieldComments() throws IOException {
class A extends YamlConfiguration {
@Comment("aB")
private int aB = 1;
@Comment({"cD", "dC"})
private int cD = 2;
protected A(YamlProperties properties) {
super(testPath, properties);
}
}
YamlProperties properties = YamlProperties.builder()
.setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
.build();
new A(properties).save();
assertThat(readConfig(testPath), is(FORMATTED_FIELD_COMMENTS_YML));
}
private String readConfig(Path path) throws IOException {
return Files.lines(path).collect(joining("\n"));
}
private static final String PRE_AND_APPENDED_COMMENTS_YML = "# AB\n" +
"\n" +
"# CD\n" +
"i: 0\n" +
"# AB\n" +
"\n" +
"# CD";
private static final String FIELD_COMMENTS_YML = "# a\n" +
"a: 1\n" +
"# b\n" +
"# x\n" +
"b: 2\n" +
"# c\n" +
"\n" +
"# y\n" +
"c: 3\n" +
"d: 4";
private static final String FORMATTED_FIELD_COMMENTS_YML = "# aB\n" +
"a_b: 1\n" +
"# cD\n" +
"# dC\n" +
"c_d: 2";
private static final String CLASS_COMMENTS_YML = "# 1\n" +
"\n" +
"# 2\n" +
"# a\n" +
"a: 1\n" +
"# b\n" +
"# x\n" +
"b: 2\n" +
"# c\n" +
"\n" +
"# y\n" +
"c: 3\n" +
"d: 4";
private static final String TEST_CLASS_YML = "# A\n" +
"\n" +
"# B\n" +
"# C\n" +
"# A\n" +
"primBool: true\n" +
"# B\n" +
"# C\n" +
"refBool: true\n" +
"# D\n" +
"\n" +
"# E\n" +
"primByte: 1\n" +
"refByte: 2\n" +
"# F\n" +
"primChar: c\n" +
"\n" +
"# G\n" +
"refChar: d\n" +
"primShort: 3\n" +
"refShort: 4\n" +
"primInt: 5\n" +
"refInt: 6\n" +
"primLong: 7\n" +
"refLong: 8\n" +
"primFloat: 9.0\n" +
"refFloat: 10.0\n" +
"primDouble: 11.0\n" +
"refDouble: 12.0\n" +
"string: string\n" +
"subClass:\n" +
" primInt: 1\n" +
" string: string\n" +
" list:\n" +
" - list\n" +
" set: !!set\n" +
" set: null\n" +
" map:\n" +
" map: 1\n" +
" testSubSubClass:\n" +
" primInt: 14\n" +
" string: '14'\n" +
" list:\n" +
" - '1414'\n" +
" set: !!set\n" +
" '1414': null\n" +
" map:\n" +
" '1414': 14\n" +
" subClassList:\n" +
" - primInt: 15\n" +
" string: '15'\n" +
" list:\n" +
" - '1515'\n" +
" set: !!set\n" +
" '1515': null\n" +
" map:\n" +
" '1515': 15\n" +
" - primInt: 16\n" +
" string: '16'\n" +
" list:\n" +
" - '1616'\n" +
" set: !!set\n" +
" '1616': null\n" +
" map:\n" +
" '1616': 16\n" +
" subClassSet: !!set\n" +
" ? primInt: 18\n" +
" string: '18'\n" +
" list:\n" +
" - '1818'\n" +
" set: !!set\n" +
" '1818': null\n" +
" map:\n" +
" '1818': 18\n" +
" : null\n" +
" ? primInt: 17\n" +
" string: '17'\n" +
" list:\n" +
" - '1717'\n" +
" set: !!set\n" +
" '1717': null\n" +
" map:\n" +
" '1717': 17\n" +
" : null\n" +
" subClassMap:\n" +
" map:\n" +
" primInt: 19\n" +
" string: '19'\n" +
" list:\n" +
" - '1919'\n" +
" set: !!set\n" +
" '1919': null\n" +
" map:\n" +
" '1919': 19\n" +
"ints: !!set\n" +
" 1: null\n" +
" 2: null\n" +
" 3: null\n" +
"strings:\n" +
"- a\n" +
"- b\n" +
"- c\n" +
"doubleByBool:\n" +
" true: 1.0\n" +
" false: 2.0\n" +
"subClassSet: !!set\n" +
" ? primInt: 1\n" +
" string: '1'\n" +
" list:\n" +
" - '1'\n" +
" set: !!set\n" +
" '1': null\n" +
" map:\n" +
" '1': 1\n" +
" testSubSubClass:\n" +
" primInt: 1\n" +
" string: '1'\n" +
" list:\n" +
" - '11'\n" +
" set: !!set\n" +
" '11': null\n" +
" map:\n" +
" '11': 1\n" +
" subClassList:\n" +
" - primInt: 100\n" +
" string: '1'\n" +
" list:\n" +
" - '11'\n" +
" set: !!set\n" +
" '11': null\n" +
" map:\n" +
" '11': 100\n" +
" subClassSet: !!set\n" +
" ? primInt: 101\n" +
" string: '1'\n" +
" list:\n" +
" - '11'\n" +
" set: !!set\n" +
" '11': null\n" +
" map:\n" +
" '11': 101\n" +
" : null\n" +
" subClassMap:\n" +
" '1':\n" +
" primInt: 102\n" +
" string: '1'\n" +
" list:\n" +
" - '11'\n" +
" set: !!set\n" +
" '11': null\n" +
" map:\n" +
" '11': 102\n" +
" : null\n" +
" ? primInt: 2\n" +
" string: '2'\n" +
" list:\n" +
" - '2'\n" +
" set: !!set\n" +
" '2': null\n" +
" map:\n" +
" '2': 2\n" +
" testSubSubClass:\n" +
" primInt: 2\n" +
" string: '2'\n" +
" list:\n" +
" - '22'\n" +
" set: !!set\n" +
" '22': null\n" +
" map:\n" +
" '22': 2\n" +
" subClassList:\n" +
" - primInt: 200\n" +
" string: '2'\n" +
" list:\n" +
" - '22'\n" +
" set: !!set\n" +
" '22': null\n" +
" map:\n" +
" '22': 200\n" +
" subClassSet: !!set\n" +
" ? primInt: 202\n" +
" string: '2'\n" +
" list:\n" +
" - '22'\n" +
" set: !!set\n" +
" '22': null\n" +
" map:\n" +
" '22': 202\n" +
" : null\n" +
" subClassMap:\n" +
" '2':\n" +
" primInt: 204\n" +
" string: '2'\n" +
" list:\n" +
" - '22'\n" +
" set: !!set\n" +
" '22': null\n" +
" map:\n" +
" '22': 204\n" +
" : null\n" +
"subClassList:\n" +
"- primInt: 3\n" +
" string: '3'\n" +
" list:\n" +
" - '3'\n" +
" set: !!set\n" +
" '3': null\n" +
" map:\n" +
" '3': 3\n" +
" testSubSubClass:\n" +
" primInt: 3\n" +
" string: '3'\n" +
" list:\n" +
" - '33'\n" +
" set: !!set\n" +
" '33': null\n" +
" map:\n" +
" '33': 3\n" +
" subClassList:\n" +
" - primInt: 300\n" +
" string: '3'\n" +
" list:\n" +
" - '33'\n" +
" set: !!set\n" +
" '33': null\n" +
" map:\n" +
" '33': 300\n" +
" subClassSet: !!set\n" +
" ? primInt: 303\n" +
" string: '3'\n" +
" list:\n" +
" - '33'\n" +
" set: !!set\n" +
" '33': null\n" +
" map:\n" +
" '33': 303\n" +
" : null\n" +
" subClassMap:\n" +
" '3':\n" +
" primInt: 306\n" +
" string: '3'\n" +
" list:\n" +
" - '33'\n" +
" set: !!set\n" +
" '33': null\n" +
" map:\n" +
" '33': 306\n" +
"- primInt: 4\n" +
" string: '4'\n" +
" list:\n" +
" - '4'\n" +
" set: !!set\n" +
" '4': null\n" +
" map:\n" +
" '4': 4\n" +
" testSubSubClass:\n" +
" primInt: 4\n" +
" string: '4'\n" +
" list:\n" +
" - '44'\n" +
" set: !!set\n" +
" '44': null\n" +
" map:\n" +
" '44': 4\n" +
" subClassList:\n" +
" - primInt: 400\n" +
" string: '4'\n" +
" list:\n" +
" - '44'\n" +
" set: !!set\n" +
" '44': null\n" +
" map:\n" +
" '44': 400\n" +
" subClassSet: !!set\n" +
" ? primInt: 404\n" +
" string: '4'\n" +
" list:\n" +
" - '44'\n" +
" set: !!set\n" +
" '44': null\n" +
" map:\n" +
" '44': 404\n" +
" : null\n" +
" subClassMap:\n" +
" '4':\n" +
" primInt: 408\n" +
" string: '4'\n" +
" list:\n" +
" - '44'\n" +
" set: !!set\n" +
" '44': null\n" +
" map:\n" +
" '44': 408\n" +
"subClassMap:\n" +
" '5':\n" +
" primInt: 5\n" +
" string: '5'\n" +
" list:\n" +
" - '5'\n" +
" set: !!set\n" +
" '5': null\n" +
" map:\n" +
" '5': 5\n" +
" testSubSubClass:\n" +
" primInt: 5\n" +
" string: '5'\n" +
" list:\n" +
" - '55'\n" +
" set: !!set\n" +
" '55': null\n" +
" map:\n" +
" '55': 5\n" +
" subClassList:\n" +
" - primInt: 500\n" +
" string: '5'\n" +
" list:\n" +
" - '55'\n" +
" set: !!set\n" +
" '55': null\n" +
" map:\n" +
" '55': 500\n" +
" subClassSet: !!set\n" +
" ? primInt: 505\n" +
" string: '5'\n" +
" list:\n" +
" - '55'\n" +
" set: !!set\n" +
" '55': null\n" +
" map:\n" +
" '55': 505\n" +
" : null\n" +
" subClassMap:\n" +
" '5':\n" +
" primInt: 510\n" +
" string: '5'\n" +
" list:\n" +
" - '55'\n" +
" set: !!set\n" +
" '55': null\n" +
" map:\n" +
" '55': 510\n" +
" '6':\n" +
" primInt: 6\n" +
" string: '6'\n" +
" list:\n" +
" - '6'\n" +
" set: !!set\n" +
" '6': null\n" +
" map:\n" +
" '6': 6\n" +
" testSubSubClass:\n" +
" primInt: 6\n" +
" string: '6'\n" +
" list:\n" +
" - '66'\n" +
" set: !!set\n" +
" '66': null\n" +
" map:\n" +
" '66': 6\n" +
" subClassList:\n" +
" - primInt: 600\n" +
" string: '6'\n" +
" list:\n" +
" - '66'\n" +
" set: !!set\n" +
" '66': null\n" +
" map:\n" +
" '66': 600\n" +
" subClassSet: !!set\n" +
" ? primInt: 606\n" +
" string: '6'\n" +
" list:\n" +
" - '66'\n" +
" set: !!set\n" +
" '66': null\n" +
" map:\n" +
" '66': 606\n" +
" : null\n" +
" subClassMap:\n" +
" '6':\n" +
" primInt: 612\n" +
" string: '6'\n" +
" list:\n" +
" - '66'\n" +
" set: !!set\n" +
" '66': null\n" +
" map:\n" +
" '66': 612\n" +
"listsList:\n" +
"- - 1\n" +
" - 2\n" +
"- - 3\n" +
" - 4\n" +
"setsSet: !!set\n" +
" ? !!set\n" +
" a: null\n" +
" b: null\n" +
" : null\n" +
" ? !!set\n" +
" c: null\n" +
" d: null\n" +
" : null\n" +
"mapsMap:\n" +
" 1:\n" +
" '1': 1\n" +
" 2:\n" +
" '2': 2\n" +
"subClassListsList:\n" +
"- - primInt: 7\n" +
" string: '7'\n" +
" list:\n" +
" - '7'\n" +
" set: !!set\n" +
" '7': null\n" +
" map:\n" +
" '7': 7\n" +
" testSubSubClass:\n" +
" primInt: 7\n" +
" string: '7'\n" +
" list:\n" +
" - '77'\n" +
" set: !!set\n" +
" '77': null\n" +
" map:\n" +
" '77': 7\n" +
" subClassList:\n" +
" - primInt: 700\n" +
" string: '7'\n" +
" list:\n" +
" - '77'\n" +
" set: !!set\n" +
" '77': null\n" +
" map:\n" +
" '77': 700\n" +
" subClassSet: !!set\n" +
" ? primInt: 707\n" +
" string: '7'\n" +
" list:\n" +
" - '77'\n" +
" set: !!set\n" +
" '77': null\n" +
" map:\n" +
" '77': 707\n" +
" : null\n" +
" subClassMap:\n" +
" '7':\n" +
" primInt: 714\n" +
" string: '7'\n" +
" list:\n" +
" - '77'\n" +
" set: !!set\n" +
" '77': null\n" +
" map:\n" +
" '77': 714\n" +
" - primInt: 8\n" +
" string: '8'\n" +
" list:\n" +
" - '8'\n" +
" set: !!set\n" +
" '8': null\n" +
" map:\n" +
" '8': 8\n" +
" testSubSubClass:\n" +
" primInt: 8\n" +
" string: '8'\n" +
" list:\n" +
" - '88'\n" +
" set: !!set\n" +
" '88': null\n" +
" map:\n" +
" '88': 8\n" +
" subClassList:\n" +
" - primInt: 800\n" +
" string: '8'\n" +
" list:\n" +
" - '88'\n" +
" set: !!set\n" +
" '88': null\n" +
" map:\n" +
" '88': 800\n" +
" subClassSet: !!set\n" +
" ? primInt: 808\n" +
" string: '8'\n" +
" list:\n" +
" - '88'\n" +
" set: !!set\n" +
" '88': null\n" +
" map:\n" +
" '88': 808\n" +
" : null\n" +
" subClassMap:\n" +
" '8':\n" +
" primInt: 816\n" +
" string: '8'\n" +
" list:\n" +
" - '88'\n" +
" set: !!set\n" +
" '88': null\n" +
" map:\n" +
" '88': 816\n" +
"subClassSetsSet: !!set\n" +
" ? !!set\n" +
" ? primInt: 10\n" +
" string: '10'\n" +
" list:\n" +
" - '10'\n" +
" set: !!set\n" +
" '10': null\n" +
" map:\n" +
" '10': 10\n" +
" testSubSubClass:\n" +
" primInt: 10\n" +
" string: '10'\n" +
" list:\n" +
" - '1010'\n" +
" set: !!set\n" +
" '1010': null\n" +
" map:\n" +
" '1010': 10\n" +
" subClassList:\n" +
" - primInt: 1000\n" +
" string: '10'\n" +
" list:\n" +
" - '1010'\n" +
" set: !!set\n" +
" '1010': null\n" +
" map:\n" +
" '1010': 1000\n" +
" subClassSet: !!set\n" +
" ? primInt: 1010\n" +
" string: '10'\n" +
" list:\n" +
" - '1010'\n" +
" set: !!set\n" +
" '1010': null\n" +
" map:\n" +
" '1010': 1010\n" +
" : null\n" +
" subClassMap:\n" +
" '10':\n" +
" primInt: 1020\n" +
" string: '10'\n" +
" list:\n" +
" - '1010'\n" +
" set: !!set\n" +
" '1010': null\n" +
" map:\n" +
" '1010': 1020\n" +
" : null\n" +
" ? primInt: 9\n" +
" string: '9'\n" +
" list:\n" +
" - '9'\n" +
" set: !!set\n" +
" '9': null\n" +
" map:\n" +
" '9': 9\n" +
" testSubSubClass:\n" +
" primInt: 9\n" +
" string: '9'\n" +
" list:\n" +
" - '99'\n" +
" set: !!set\n" +
" '99': null\n" +
" map:\n" +
" '99': 9\n" +
" subClassList:\n" +
" - primInt: 900\n" +
" string: '9'\n" +
" list:\n" +
" - '99'\n" +
" set: !!set\n" +
" '99': null\n" +
" map:\n" +
" '99': 900\n" +
" subClassSet: !!set\n" +
" ? primInt: 909\n" +
" string: '9'\n" +
" list:\n" +
" - '99'\n" +
" set: !!set\n" +
" '99': null\n" +
" map:\n" +
" '99': 909\n" +
" : null\n" +
" subClassMap:\n" +
" '9':\n" +
" primInt: 918\n" +
" string: '9'\n" +
" list:\n" +
" - '99'\n" +
" set: !!set\n" +
" '99': null\n" +
" map:\n" +
" '99': 918\n" +
" : null\n" +
" : null\n" +
"subClassMapsMap:\n" +
" 1:\n" +
" '1':\n" +
" primInt: 11\n" +
" string: '11'\n" +
" list:\n" +
" - '11'\n" +
" set: !!set\n" +
" '11': null\n" +
" map:\n" +
" '11': 11\n" +
" testSubSubClass:\n" +
" primInt: 11\n" +
" string: '11'\n" +
" list:\n" +
" - '1111'\n" +
" set: !!set\n" +
" '1111': null\n" +
" map:\n" +
" '1111': 11\n" +
" subClassList:\n" +
" - primInt: 1100\n" +
" string: '11'\n" +
" list:\n" +
" - '1111'\n" +
" set: !!set\n" +
" '1111': null\n" +
" map:\n" +
" '1111': 1100\n" +
" subClassSet: !!set\n" +
" ? primInt: 1111\n" +
" string: '11'\n" +
" list:\n" +
" - '1111'\n" +
" set: !!set\n" +
" '1111': null\n" +
" map:\n" +
" '1111': 1111\n" +
" : null\n" +
" subClassMap:\n" +
" '11':\n" +
" primInt: 1122\n" +
" string: '11'\n" +
" list:\n" +
" - '1111'\n" +
" set: !!set\n" +
" '1111': null\n" +
" map:\n" +
" '1111': 1122\n" +
" 2:\n" +
" '2':\n" +
" primInt: 12\n" +
" string: '12'\n" +
" list:\n" +
" - '12'\n" +
" set: !!set\n" +
" '12': null\n" +
" map:\n" +
" '12': 12\n" +
" testSubSubClass:\n" +
" primInt: 12\n" +
" string: '12'\n" +
" list:\n" +
" - '1212'\n" +
" set: !!set\n" +
" '1212': null\n" +
" map:\n" +
" '1212': 12\n" +
" subClassList:\n" +
" - primInt: 1200\n" +
" string: '12'\n" +
" list:\n" +
" - '1212'\n" +
" set: !!set\n" +
" '1212': null\n" +
" map:\n" +
" '1212': 1200\n" +
" subClassSet: !!set\n" +
" ? primInt: 1212\n" +
" string: '12'\n" +
" list:\n" +
" - '1212'\n" +
" set: !!set\n" +
" '1212': null\n" +
" map:\n" +
" '1212': 1212\n" +
" : null\n" +
" subClassMap:\n" +
" '12':\n" +
" primInt: 1224\n" +
" string: '12'\n" +
" list:\n" +
" - '1212'\n" +
" set: !!set\n" +
" '1212': null\n" +
" map:\n" +
" '1212': 1224\n" +
"e1: NON_DEFAULT\n" +
"enums:\n" +
"- DEFAULT\n" +
"- NON_DEFAULT\n" +
"converterSubClass: '13:13'\n" +
"excludedClass: !!de.exlll.configlib.classes.TestExcludedClass\n" +
" primInt: 1\n" +
" string: string";
}

@ -1,42 +0,0 @@
package de.exlll.configlib.configs.yaml;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.classes.TestClass;
import de.exlll.configlib.configs.yaml.YamlConfiguration.YamlProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static de.exlll.configlib.util.CollectionFactory.mapOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
class YamlSourceTest {
private FileSystem fileSystem;
private Path configPath;
@BeforeEach
void setUp() {
fileSystem = Jimfs.newFileSystem();
configPath = fileSystem.getPath("/a/b/config.yml");
}
@AfterEach
void tearDown() throws IOException {
fileSystem.close();
}
@Test
void yamlSourceCreatesDirectories() throws IOException {
YamlSource source = new YamlSource(configPath, YamlProperties.DEFAULT);
Path parentDir = configPath.getParent();
assertThat(Files.exists(parentDir), is(false));
source.saveConfiguration(new TestClass(configPath), mapOf());
assertThat(Files.exists(parentDir), is(true));
}
}

@ -1,58 +0,0 @@
package de.exlll.configlib.filter;
import de.exlll.configlib.classes.ClassWithFinalStaticTransientField;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class FieldFilterTest {
private static final FieldFilter filter = FieldFilters.DEFAULT;
private static final Class<ClassWithFinalStaticTransientField> CWFSTF =
ClassWithFinalStaticTransientField.class;
@Test
void filteredFieldsFiltersFields() throws NoSuchFieldException {
List<? extends Field> fields = filter.filterDeclaredFieldsOf(CWFSTF);
assertThat(fields.size(), is(0));
class A {
private int i;
private final int j = 0;
private transient int k;
}
fields = filter.filterDeclaredFieldsOf(A.class);
assertThat(fields.size(), is(1));
assertThat(fields.get(0), is(A.class.getDeclaredField("i")));
}
@Test
void defaultFilterFiltersSyntheticFields() {
for (Field field : ClassWithSyntheticField.class.getDeclaredFields()) {
assertThat(field.isSynthetic(), is(true));
assertThat(filter.test(field), is(false));
}
}
@Test
void defaultFilterFiltersFinalStaticTransientFields()
throws NoSuchFieldException {
Field field = CWFSTF.getDeclaredField("i");
assertThat(Modifier.isFinal(field.getModifiers()), is(true));
assertThat(filter.test(field), is(false));
field = CWFSTF.getDeclaredField("j");
assertThat(Modifier.isStatic(field.getModifiers()), is(true));
assertThat(filter.test(field), is(false));
field = CWFSTF.getDeclaredField("k");
assertThat(Modifier.isTransient(field.getModifiers()), is(true));
assertThat(filter.test(field), is(false));
}
private final class ClassWithSyntheticField {}
}

@ -1,87 +0,0 @@
package de.exlll.configlib.util;
import java.util.*;
public final class CollectionFactory {
private CollectionFactory() { throw new AssertionError(); }
@SafeVarargs
public static <T> List<T> listOf(T... values) {
return Arrays.asList(values);
}
@SafeVarargs
public static <T> Set<T> setOf(T... values) {
return new LinkedHashSet<>(Arrays.asList(values));
}
public static <K, V> Map<K, V> mapOf() {
return new LinkedHashMap<>();
}
public static <K, V> Map<K, V> mapOf(K k, V v) {
HashMap<K, V> map = new LinkedHashMap<>();
map.put(k, v);
return map;
}
public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2) {
Map<K, V> map = mapOf(k1, v1);
map.put(k2, v2);
return map;
}
public static <K, V> Map<K, V> mapOf(K k1, V v1, K k2, V v2, K k3, V v3) {
Map<K, V> map = mapOf(k1, v1, k2, v2);
map.put(k3, v3);
return map;
}
public static <K, V> Map<K, V> mapOf(
K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4
) {
Map<K, V> map = mapOf(k1, v1, k2, v2, k3, v3);
map.put(k4, v4);
return map;
}
public static <K, V> Map<K, V> mapOf(
K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5
) {
Map<K, V> map = mapOf(k1, v1, k2, v2, k3, v3, k4, v4);
map.put(k5, v5);
return map;
}
public static <K, V> Map.Entry<K, V> mapEntry(K k, V v) {
return new MapEntry<>(k, v);
}
private static final class MapEntry<K, V> implements Map.Entry<K, V> {
private final K key;
private V value;
public MapEntry(K key, V value) {
this.key = key;
this.value = value;
}
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return value;
}
@Override
public V setValue(V value) {
V old = this.value;
this.value = value;
return old;
}
}
}

@ -1,509 +1,585 @@
# ConfigLib v2
# ConfigLib
**A Bukkit and BungeeCord library for storing and loading configurations**
**A Minecraft library for mapping objects to YAML configuration files.**
This library facilitates creating, saving and loading configurations by reflectively converting configuration
instances to serializable `Map`s which can be transformed to different representations (e.g. YAML) before being
stored to files or other storage systems.
This library facilitates creating, saving, loading, updating, and documenting YAML configuration
files. It does so by automatically mapping instances of configuration classes to serializable maps
which are then transformed into YAML and saved to some specified file.
Currently this library only supports storing configurations as YAML. However, users may provide their own
storage systems.
## Features
For a step-by-step tutorial see: [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
* Automatic creation, saving, loading, and updating of configuration files
* Support for all primitive types, their wrapper types, and Strings
* Support for `BigInteger`, `BigDecimal`, `LocalDate`, `LocalTime`, and `LocalDateTime`
* Support for (nested) lists, sets, arrays, and maps
* Support for enums and POJOs (+ inheritance!)
* Option to exclude fields from being converted
* Option to add explanatory comments by annotating fields
* Option to format field names before conversion
* Option to customize null handling
* Option to provide custom serializers
* ...and a few more!
## Features
* automatic creation, saving, loading and updating of configurations
* (_YAML_) automatic creation of files and directories
* support for all primitive types, their wrapper types and `String`s
* support for (nested) `List`s, `Set`s and `Map`s
* support for `Enum`s and POJOs
* option to add explanatory comments by annotating classes and their fields
* option to provide custom configuration sources
* option to exclude fields from being converted
* option to provide custom conversion mechanisms
* option to format field names before conversion
* option to execute action before/after loading/saving the configuration
* (_YAML_) option to change the style of the configuration file
* (_YAML_) option to prepend/append text (e.g. color codes)
## Usage example
## General information
#### Supported types
By default, the following types are converted automatically:
- simple types, i.e. primitive types, their wrapper types and `String`s
- `Enum`s
- any type that is annotated as a `ConfigurationElement`
- (nested) `List`s, `Set`s and `Map`s of all the above (e.g. `List<SomeType>`, `Map<String, List<SomeEnum>>`)
- only simple types can be `Map` keys
For fields whose types are not any of the above, you have two other options:
* Add a custom `Converter` that converts the field's value to any of the above types and back from it.
* If the underlying storage system can handle the type, exclude the field from being converted.
#### Null values
This library does _not_ support `null` values. All non-primitive fields (e.g. `Integer`, `String`, `List`, `Enum`s)
must be assigned non-null default values.
#### Adding or removing configuration options
This library supports adding or removing configuration options by simply adding new fields to or removing old fields
from the configuration class. The next time the `save` or `loadAndSave` method is called, the changes will be saved.
#### Changing the type of configuration options
Changing the type of configuration options is **_not_** supported. **Don't do that.** This may lead to
`ClassCastException`s when loading or accessing the field. This is especially important for generic fields.
For example, you should never change a `List<String>` to a `List<Integer>`.
If you need the type of an option to change, add a new field with a different name and the desired type and then
remove the old one.
#### Subclassing configurations
Currently, subclassing configurations is not supported. If you have an instance of class `B` where `B` is a
subclass of `A` and `A` is a subclass of `YamlConfiguration` and you save or load that instance, then only the
fields of class `B` will be saved or loaded, respectively.
## How-to (_YAML_)
For a step-by-step tutorial see: [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
#### Creating configurations
To create a YAML configuration, create a new class and extend `YamlConfiguration`. If you write a Bukkit plugin,
you can alternatively extend `BukkitYamlConfiguration` which is a subclass of `YamlConfiguration` and can
properly convert Bukkit classes like `Inventory` and `ItemStack` to YAML.
#### Instantiating configurations
* To instantiate a `YamlConfiguration`, you need to pass a `Path` and optionally a `YamlConfiguration.YamlProperties`
object to its constructor.
* To instantiate a `BukkitYamlConfiguration`, you need to pass a `Path` and optionally a
`BukkitYamlConfiguration.BukkitYamlProperties` object to its constructor.
If you don't pass a `(Bukkit-)YamlProperties` object, the `(Bukkit-)YamlProperties.DEFAULT` instance will be used.
#### Instantiating (Bukkit-)YamlProperties
To instantiate a new `(Bukkit-)YamlProperties` object, call `(Bukkit-)YamlProperties.builder()`,
configure the builder and then call its `build()` method.
Note: The `BukkitYamlProperties` is a subclass of `YamlProperties` but doesn't add any new methods to it.
Its sole purpose is to provide more appropriate defaults to the underlying YAML parser.
#### Saving and loading configurations
Instances of your configuration class have a `load`, `save` and `loadAndSave` method:
- `load` first tries to load the configuration file and then updates the values of all fields of the configuration
instance with the values it read from the file.
* If the file contains an entry that doesn't have a corresponding field, the entry is ignored.
* If the instance contains a field for which no entry was found, the default value you assigned to that field is kept.
- `save` first converts the configuration instance with its current values to YAML and then tries to dump that YAML
to a configuration file.
* The configuration file is completely overwritten. This means any entries it contains are lost afterwards.
- `loadAndSave` is a convenience method that first calls `load` and then `save`.
* If the file doesn't exist, the configuration instance keeps its default values. Otherwise, the values are
updated with the values read from the file.
* Subsequently the instance is saved so that the values of any newly added fields are also added
to configuration file.
#### Adding and removing fields
Adding and removing fields is supported. However, changing the type of field is not.
For example, you can change the following `YamlConfiguration`
```java
class MyConfiguration extends YamlConfiguration {
private String s = "1";
private double d = 4.2;
// ...
}
```
to this:
```java
class MyConfiguration extends YamlConfiguration {
private String s = "2";
private int i = 1;
// ...
}
```
But you are not allowed to change the type of the variable `d` to `int` (or any other type).
This section contains a short usage example to get you started. The whole range of features is
discussed in the following sections. Information on how to import this library is located at the end
of this documentation.
#### Simple, enum and custom types
The following types are simple types (remember that `null` values are not allowed):
```java
class MyConfiguration extends YamlConfiguration {
private boolean primBool;
private Boolean refBool = false;
private byte primByte;
private Byte refByte = 0;
private char primChar;
private Character refChar = '\0';
private short primShort;
private Short refShort = 0;
private int primInt;
private Integer refInt = 0;
private long primLong;
private Long refLong = 0L;
private float primFloat;
private Float refFloat = 0F;
private double primDouble;
private Double refDouble = 0.0;
private String string = "";
// ...
}
```
public final class Example {
// To create a configuration annotate the class with @Configuration and make sure that
// it has a no-args constructor. That's it! Now you can add fields to it which can all
// be private; setters are not required!
@Configuration
public static class BaseConfiguration {
private String host = "127.0.0.1";
private int port = 1234;
// The library supports lists, sets, and maps.
private Set<String> blockedAddresses = Set.of();
// Fields can be ignored by making them final, transient, static or by
// annotating them with @Ignore.
private final double ignoreMe = 3.14;
}
Enums are supported:
// This class does not need to be annotated with @Configuration because it
// extends a class which already is!
public static final class UserConfiguration extends BaseConfiguration {
// You can add comments with the @Comment annotation. Each string in the comment
// array is written (as a comment) on a new line.
@Comment({"The admin user has full access.", "Choose a proper password!"})
User admin = new User("root", "toor"); // The User class is a @Configuration!
List<User> blockedUsers = List.of(
new User("user1", null), // null values are supported
new User("user2", null)
);
}
```java
class MyConfiguration extends YamlConfiguration {
private Material material = Material.AIR;
//...
}
```
@Configuration
public static final class User {
private String username;
@Comment("Please choose a strong password.")
private String password;
Custom classes are supported if they are annotated as `ConfigurationElement`s and if they have a no-args constructor.
Custom classes can have fields whose values are also instances of custom classes.
// Configuration classes require a no-args constructor.
// The constructor can be private though.
private User() {}
```java
@ConfigurationElement
class MyCustomClass1 {/* fields etc.*/}
public User(String username, String password) {/* initialize */}
}
@ConfigurationElement
class MyCustomClass2 {
private MyCustomClass1 cls1 = new MyCustomClass1();
// ...
}
public static void main(String[] args) {
final var props = YamlConfigurationProperties.newBuilder().build();
final var store = new YamlConfigurationStore<>(UserConfiguration.class, props);
final var configFile = Paths.get("/tmp/config.yml");
// Save a new instance to the configuration file
store.save(new UserConfiguration(), configFile);
class MyConfiguration extends YamlConfiguration {
private MyCustomClass2 cls2 = new MyCustomClass2();
// ...
// Load a new instance from the configuration file
UserConfiguration configuration = store.load(configFile);
System.out.println(configuration.admin.username);
System.out.println(configuration.blockedUsers);
// Modify and save the configuration file
configuration.blockedUsers.add(new User("user3", "pass3"));
store.save(configuration, configFile);
}
}
```
#### `List`s, `Set`s, `Map`s
Lists, sets and maps of simple types can be used as is and don't need any special treatment.
```java
class MyConfiguration extends YamlConfiguration {
private Set<Integer> ints = new HashSet<>();
private List<String> strings = new ArrayList<>();
private Map<Boolean, Double> doubleByBool = new HashMap<>();
// ...
}
By running the above code, a new YAML configuration is created at `/tmp/config.yml`. Its content
looks like this:
```yaml
host: 127.0.0.1
port: 1234
blockedAddresses: [ ]
# The admin user has full access.
# Choose a proper password!
admin:
username: root
# Please choose a strong password.
password: toor
blockedUsers:
- username: user1
- username: user2
- username: user3
password: pass3
```
Note: Even though sets are supported, their YAML-representation is 'pretty ugly', so it's better to use lists instead.
If you need set behavior, you can internally use lists and convert them to sets using the `preSave/postLoad`-hooks.
Two things are noticeable here:
1. Not every user in the `blockedUsers` list has a `password` mapping. This is because null values
are not output by default. That behavior can be changed by the builder.
2. The password of the user with username `user3` that has no comment. This is due to limitations of
the YAML library. Configurations in lists, sets, or maps cannot have their comments printed.
## General information
In the following sections the term _configuration type_ refers to any non-generic class that is
directly or indirectly (i.e. through subclassing) annotated with
`@de.exlll.configlib.Configuration`. Accordingly, the term _configuration_ refers to an instance of
such a type.
### Declaring configuration types
To declare a configuration type, annotate a class with `@Configuration` and make sure that it has a
no-args constructor. The no-args constructor can be set `private`. Inner classes (i.e. the ones that
are nested but not `static`) have an implicit synthetic constructor with at least one argument and
are therefore not supported.
Add fields to your class whose type is any of the supported types listed in the next section. You
should initialize all fields of reference types with non-null default values, though you can leave
them null. Handling of null values is discussed in one of the sections further below.
Lists, sets and maps that contain other types (e.g. custom types or enums) must use the `@ElementType` annotation.
Only simple types can be used as map keys.
### Supported types
A configuration type may only contain fields of the following types:
| Type class | Types |
|------------------------|--------------------------------------------------------------------|
| Boolean types | `boolean`, and `Boolean` |
| Integer types | `byte`, `short`, `int`, `long`, and their respective wrapper types |
| Floating point types | `float`, `double`, and their respective wrapper types |
| Characters and strings | `char`, `Character`, `String` |
| Big numeric types | `BigInteger`, `BigDecimal` |
| Time related types | `LocalTime`, `LocalDate`, `LocalDateTime` |
| Enums | Any Java enum |
| Configurations | Any configuration type |
| Collections | (Nested) Lists, sets, maps*, or arrays of previously listed types |
(*) Map keys cannot be in the `Configurations` or `Collections` type class.
<details>
<summary>Examples of supported types</summary>
The following class contains examples of types that this library supports:
```java
@ConfigurationElement
class MyCustomClass {/* fields etc.*/}
class MyConfiguration extends YamlConfiguration {
@ElementType(Material.class)
private List<Material> materials = new ArrayList<>();
@ElementType(MyCustomClass.class)
private Set<MyCustomClass> customClasses = new HashSet<>();
@ElementType(MyCustomClass.class)
private Map<String, MyCustomClass> customClassesMap = new HashMap<>();
// ...
public final class SupportedTypes {
boolean supported;
Character supported;
ExampleEnum supported; // where 'ExampleEnum' is some Java enum type
ExampleConf supported; // where 'ExampleConf' is another configuration type
java.awt.Point supported; // only if a custom serializer is registered
/* collection types */
List<BigInteger> supported;
Set<Double> supported;
LocalDate[] supported;
Map<ExampleEnum, ExampleConf> supported;
/* nested collection types */
List<Map<ExampleEnum, LocalDate>> supported;
int[][] supported;
Map<Integer, List<Map<Short, Set<ExampleConf>>>> supported;
}
```
Lists, sets and maps can be nested. If nested collections contain custom types, you must specify the
nesting level using the `@ElementType` annotation. Examples:
</details>
* `List<T>` requires a nesting level of 0, which is the default value, so you don't have to set it
* `List<List<T>>` requires a nesting level of 1
* `List<List<List<T>>>` requires a nesting level of 2
* `List<Map<String, T>>` requires a nesting level of 1
* `List<Map<String, Map<String, T>>>` requires a nesting level of 2
<details>
<summary>Examples of unsupported types</summary>
The following class contains examples of types that this library does (and will) not support:
```java
@ConfigurationElement
class MyCustomClass {/* fields etc.*/}
class MyConfiguration extends YamlConfiguration {
private List<List<Integer>> listsList = new ArrayList<>();
private Set<Set<String>> setsSet = new HashSet<>();
private Map<Integer, Map<String, Integer>> mapsMap = new HashMap<>();
@ElementType(value = MyCustomClass.class, nestingLevel = 1)
private List<List<MyCustomClass>> customClassListsList = new ArrayList<>();
@ElementType(value = MyCustomClass.class, nestingLevel = 1)
private Set<Set<MyCustomClass>> customClassSetsSet = new HashSet<>();
@ElementType(value = MyCustomClass.class, nestingLevel = 1)
private Map<Integer, Map<String, MyCustomClass>> customClassMapsMap = new HashMap<>();
// ...
public final class UnsupportedTypes<T> {
Map<Point, String> unsupported; // invalid map key
Map<List<String>, String> unsupported; // invalid map key
Box<String> unsupported; // custom parameterized type
List<? extends String> unsupported; // wildcard type
List<?> unsupported; // wildcard type
List<?>[] unsupported; // wildcard type
T unsupported; // type variable
List unsupported; // raw type
List[] unsupported; // raw type
List<String>[] unsupported; // generic array type
Set<Integer>[] unsupported; // generic array type
Map<Byte, Byte>[] unsupported; // generic array type
}
```
#### Adding comments
You can add comments to a configuration class or a its field by using the `@Comment` annotation.
Class comments are saved at the beginning of a configuration file.
```java
@Comment({"A", "", "B"})
class MyConfiguration extends YamlConfiguration {
@Comment("the x")
private int x;
@Comment({"", "the y"})
private int y;
// ...
</details>
### Loading and saving configurations
There are two ways to load and save configurations. Which way you choose depends on your liking.
Both ways have three methods in common:
* `save` saves a configuration to a file
* `load` creates a new configuration instance and populates it with values taken from a file
* `update` is a combination of `load` and `save` and the method you'd usually want to use: it takes
care of creating the configuration file if it does not exist and updates it otherwise to reflect
changes to (the fields of) the configuration type.
<details>
<summary>Example of <code>update</code> behavior when configuration file exists</summary>
Let's say you have the following configuration type:
```java
@Configuration
public final class C {
int i = 10;
int j = 11;
}
```
Empty strings are represented as newlines (i.e. lines that don't start with '# ').
#### Executing pre-save and post-load actions
To execute pre-save and post-load actions, override `preSave()` and `postLoad()`, respectively.
```java
class MyConfiguration extends YamlConfiguration {
@Override
protected void preSave(){ /* do something ... */}
@Override
protected void postLoad(){ /* do something ... */}
// ...
}
... and a YAML configuration file that contains:
```yaml
i: 20
k: 30
```
#### Excluding fields from being converted
To exclude fields from being converted, annotate them with the `@NoConvert` annotation. This may be useful if the
configuration knows how to (de-)serialize instances of that type. For example, a `BukkitYamlConfiguration` knows how
to serialize `ItemStack` instances.
Now, when you use one of the methods below to call `update` for that configuration type and file,
the configuration instance that `update` returns will have its `i` variable initialized to `20`
and its `j` variable will have its default of `11`. After the operation, the configuration file will
contain:
```java
class MyConfiguration extends BukkitYamlConfiguration {
@NoConvert
private ItemStack itemStack = new ItemStack(Material.STONE, 1);
// ...
}
```yaml
i: 20
j: 11
```
#### Changing configuration properties
To change the properties of a configuration, use the properties builder object.
</details>
##### Formatting field names
To format field names before conversion, configure the properties builder to use a custom `FieldNameFormatter`.
You can either define your own `FieldNameFormatter` or use one from the `FieldNameFormatters` enum.
To exemplify the usage of these three methods we assume for the following sections that you have
implemented the configuration type below and have access to some regular `java.nio.file.Path`
object `configurationFile`.
```java
YamlProperties properties = YamlProperties.builder()
.setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
// ...
.build();
```java
@Configuration
public final class Config { /* some fields */ }
```
Alternatively, you can annotate your `Configuration` class with the `@Format` annotation. The `FieldNameFormatter`
returned by this annotation takes precedence over the `FieldNameFormatter` returned by a `Properties` object.
#### Way 1
```java
@Format(FieldNameFormatters.UPPER_UNDERSCORE)
class MyConfiguration extends YamlConfiguration {
// ...
}
The first way is to create a configuration store and use it directly to save, load, or update
configurations.
```java
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
YamlConfigurationStore<Config> store = new YamlConfigurationStore<>(Config.class, properties);
Config config1 = store.load(configurationFile);
store.save(config1, configurationFile);
Config config2 = store.update(configurationFile);
```
Note: You should neither remove nor replace a formatter with one that has a different formatting style because this
could break existing configurations.
#### Way 2
##### (_YAML_) Prepending/appending text
To prepend or append comments to a configuration file, use the `setPrependedComments` and `setAppendedComments` methods,
respectively.
The second way is to use the static methods from the `Configurations` class.
```java
YamlProperties properties = YamlProperties.builder()
.setPrependedComments(Arrays.asList("A", "B"))
.setAppendedComments(Arrays.asList("C", "D"))
// ...
.build();
```java
Config config1 = Configurations.loadYamlConfiguration(configurationFile, Config.class);
Configurations.saveYamlConfiguration(configurationFile, Config.class, config1);
Config config2 = Configurations.updateYamlConfiguration(configurationFile, Config.class);
```
##### (_YAML_) Changing the style of the configuration file
To change the configuration style, use the `setConstructor`, `setRepresenter`, `setOptions` and `setResolver` methods.
These methods change the behavior of the underlying YAML-parser.
See [snakeyaml-Documentation](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation).
Each of these methods has two additional overloads: One that takes a properties object and another
that lets you configure a properties object builder. For example, the overloads for the
`loadYamlConfiguration` method are:
```java
// overload 1
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.inputNulls(true)
.outputNulls(false)
.build();
Config c1 = Configurations.loadYamlConfiguration(configurationFile, Config.class, properties);
// overload 2
Config c2 = Configurations.loadYamlConfiguration(
configurationFile,
Config.class,
builder -> builder.inputNulls(true).outputNulls(false)
);
```
```java
YamlProperties properties = YamlProperties.builder()
.setConstructor(...)
.setRepresenter(/* */)
.setOptions(/* */)
.setResolver(/* */)
// ...
.build();
#### Configuration properties
Instances of the `ConfigurationProperties` class allow customization of how configurations are
stored and loaded. To create such an instance, instantiate a new builder using
the `YamlConfigurationProperties.newBuilder()` method, configure it, and finally call its `build()`
method. Check out the several methods of the builder to see which configuration options are
available.
#### Ignoring and filtering fields
Fields that are `final`, `static`, `transient` or annotated with `@Ignore` are neither serialized
nor updated during deserialization. You can filter out additional fields by providing an instance of
`FieldFilter` to the configuration properties.
#### Handling of `null` values
Configuration properties let you configure how `null` values are handled when serializing and
deserializing a configuration:
* By setting `outputNulls` to false, fields and collection elements that are null are not output.
Any comments that belong to such fields are also not written.
* By setting `inputNulls` to false, fields and collection elements that are null are not input. That
means that fields will keep their default values.
<details>
<summary>Example <code>null</code> handling configuration</summary>
The following code forbids null values to be output but allows null values to be input. By default,
both are forbidden which makes the call to `outputNulls` in this case redundant.
```java
YamlConfigurationProperties.newBuilder()
.outputNulls(false)
.inputNulls(true)
.build();
```
Note: Changing the configuration style may break adding comments using the `@Comment` annotation.
</details>
##### Adding field filters
If your configuration has a lot of fields and you want to exclude some of these fields without
making them final, static or transient, you can configure your properties object to use additional
`FieldFilter`s. A `FieldFilter` filters the fields of a configuration class by a specified criterion.
#### Field formatting
For example, if you only want to include fields whose names don't start with _ignore_, you would add
the following filter:
You can define how fields are formatted by configuring the configuration properties with a custom
formatter. Field formatters are implementations of the `FieldFormatter` interface. You can implement
this interface yourself or use one of the several formatters this library provides. These
pre-defined formatters can be found in the `FieldFormatters` class.
```java
YamlProperties properties = YamlProperties.builder()
.addFilter(field -> !field.getName().startsWith("ignore"))
// ...
.build();
<details>
<summary>Example <code>FieldFormatter</code> configuration</summary>
The following code formats fields using the `IDENTITY` formatter (which is the default).
```java
YamlConfigurationProperties.newBuilder()
.setFieldFormatter(FieldFormatters.IDENTITY)
.build();
```
Note: A filter is not evaluated for a field if the field has already been filtered or by some
other `FieldFilter`.
</details>
#### Adding custom converters
Any field can be converted using a custom converter. This can be useful if you don't like the default
conversion mechanism or if you have classes that cannot be annotated as `ConfigurationElement`s
(e.g. because they are not under your control).
### Subclassing
To create a new converter, you have to implement the `Converter<F, T>` interface where `F` represents
the type of the field and `T` represents the type of the value to which the field value is converted.
Subclassing of configurations types is supported. Subclasses of configuration types don't need to be
annotated with `@Configuration`. When a configuration is written, the fields of parent classes
are written before the fields of the child in a top to bottom manner. Parent configurations can
be `abstract`.
```java
import java.awt.Point;
#### Shadowing of fields
class PointStringConverter implements Converter<Point, String> {
@Override
public String convertTo(Point element, ConversionInfo info) {
return element.x + ":" + element.y;
}
Shadowing of fields refers to the situation where a subclass of configuration has a field that has
the same name as a field in one of its super classes. Shadowing of fields is currently not
supported. (This restriction might easily be lifted. If you need this feature, please open an issue
and describe how to handle name clashes.)
@Override
public Point convertFrom(String element, ConversionInfo info) {
String[] coordinates = element.split(":");
int x = Integer.parseInt(coordinates[0]);
int y = Integer.parseInt(coordinates[1]);
return new Point(x, y);
}
### Comments
The fields of a configuration can be annotated with the `@Comment` annotation. This annotation takes
an array of strings. Each of these strings is written onto a new line as a comment. Empty strings
are written as newlines.
<details>
<summary>Example of <code>@Comment</code> usage </summary>
Serializing the following configuration as YAML ...
```java
@Configuration
public final class ExampleConfiguration {
@Comment({"Hello", "", " ", "World"})
private String commentedField = "commented field";
}
```
To use your custom converter, pass its class to the `@Convert` annotation.
```java
class MyConfiguration extends YamlConfiguration {
@Convert(PointStringConverter.class)
private Point point = new Point(2, 3);
//...
}
... results in the YAML file shown below:
```yaml
# Hello
#
# World
commentedField: commented field
```
Note: Only a single converter instance is created which is cached.
</details>
## Example
If a configuration type _C_ that defines comments is used (as a field) within another configuration
type, the comments of _C_ are written with the proper indentation. However, if instances of _C_ are
stored inside a collection, their comments are not printed when the collection is written.
For a step-by-step tutorial of a more complex example see:
[Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
### Recursive type definitions
Recursive type definitions are currently not allowed but might be supported in a future version if
this feature is requested.
<details>
<summary>Examples of recursive type definitions</summary>
Neither direct nor indirect recursive type definitions are supported.
```java
import de.exlll.configlib.annotation.Comment;
import de.exlll.configlib.annotation.ConfigurationElement;
import de.exlll.configlib.configs.yaml.BukkitYamlConfiguration;
import de.exlll.configlib.configs.yaml.BukkitYamlConfiguration.BukkitYamlProperties;
import de.exlll.configlib.format.FieldNameFormatters;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
@ConfigurationElement
class Credentials {
private String username;
private String password;
// ConfigurationElements must have a no-args constructor
Credentials() {
this("", ""); // default values must be non-null
public final class RecursiveTypDefinitions {
// Direct recursive definition
@Configuration
static final class R {
R r;
}
Credentials(String username, String password) {
this.username = username;
this.password = password;
// Indirect recursive definition
@Configuration
static final class R1 {
R2 r2;
}
String getUsername() { return username; }
@Configuration
static final class R2 {
R1 r1;
}
}
```
</details>
### Type conversion and custom serializers
Before instances of the types listed in the [supported types](#supported-types) section can be
stored, they need to be converted into serializable types (i.e. into types the underlying YAML
library knows how to handle). The conversion happens according to the following table:
| Source type | Target type |
|------------------------|------------------|
| Boolean types | `Boolean` |
| Integer types | `Long` |
| Floating point types | `Double` |
| Characters and strings | `String` |
| Big numeric types | `String` |
| Time related types | `String` |
| Enums | `String` |
| Configurations | `Map<String, ?>` |
| `Set<S>` | `List<T>`* |
| `List<S>` | `List<T>` |
| `S[]` | `List<T>` |
| `Map<S1, S2>` | `Map<T1, T2>` |
(*) By default, sets are serialized as lists. This can be changed through the configuration
properties. This also means that `Set`s are valid target types.
#### Serializer selection
To convert the value of a field `F` with (source) type `S` into a serializable value of some
target type, a serializer has to be selected. Serializers are instances of
the `de.exlll.configlib.Serializer` interface and are selected based on `S`. Put differently,
serializers are always selected based on the compile-time type of `F` and never on the runtime type
of its value.
This distinction makes a difference (and might lead to confusion) when you have fields whose type is
a configuration type or a collection of some configuration type, and you extend that configuration
type. Concretely, assume you have and written two configuration types `A` and `B`
where `B extends A`. Then, if you use `A a = new B()` in your main configuration, only the fields of
a `A` will be stored when you save your main configuration. That is because the serializer of
field `a` was selected based on the compile-time type of `a` which is `A` and not `B`. The same
happens if you have a `List<A>` and put instances of `B` (or some other subclass of `A`) in it.
#### Custom serializers
If you want to add support for a type whose class is not annotated with `@Configuration`, you can
register a custom serializer. Serializers are instances of the `de.exlll.configlib.Serializer`
interface. When implementing that interface you have to make sure that you convert your source type
into one of the valid target types listed in the table above. The serializer then has to be
registered through a `ConfigurationProperties` object.
<details>
<summary>Example <code>Serializer</code> implementation</summary>
The following `Serializer` serializes instances of `java.awt.Point` into strings.
@Comment("MAIN-DB CONFIG")
class DatabaseConfig extends BukkitYamlConfiguration {
private String host = "localhost";
@Comment("must be greater than 1024")
private int port = 3306;
private Credentials adminAccount = new Credentials("admin", "123");
private List<String> blockedUsers = Arrays.asList("root", "john");
/* You can use the other constructor instead which uses the
* BukkitYamlProperties.DEFAULT instance. */
DatabaseConfig(Path path, BukkitYamlProperties properties) {
super(path, properties);
```java
public final class PointSerializer implements Serializer<Point, String> {
@Override
public String serialize(Point element) {
return element.x + ":" + element.y;
}
Credentials getAdminAccount() { return adminAccount; }
@Override
public Point deserialize(String element) {
String[] parts = element.split(":");
int x = Integer.parseInt(parts[0]);
int y = Integer.parseInt(parts[1]);
return new Point(x, y);
}
}
```
public final class DatabasePlugin extends JavaPlugin {
@Override
public void onEnable() {
/* Creating a properties object is not necessary if the other
* DatabaseConfig constructor is used. */
BukkitYamlProperties props = BukkitYamlProperties.builder()
.setPrependedComments(Arrays.asList("Author: Pete", "Version: 1.0"))
.setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
.build();
</details>
Path configPath = new File(getDataFolder(), "config.yml").toPath();
Custom serializers takes precedence over the serializers provided by this library.
DatabaseConfig config = new DatabaseConfig(configPath, props);
config.loadAndSave();
### Changing the type of fields
Changing the type of fields is not supported. If you change the type of one of your fields but your
configuration file still contains a value of the old type, a type mismatch will occur when reading
that file. Instead, remove the old field and add a new one with a different name.
System.out.println(config.getAdminAccount().getUsername());
}
}
```
## Import
**INFO:** I'm currently looking for an easier way for you to import this library that does not
require authentication with GitHub. Please check
this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have authentication problems.
To use this library, import it into your project with either Maven or Gradle as shown in the two
sections below. This library has additional dependencies (namely, a YAML parser) which are not
included in the artifact you import.
This repository provides plugin versions of this library which bundle all its dependencies, so you
don't have to worry about them. Also, these versions make it easier for you to update this library
if you have written multiple plugins that use it.
The plugin versions can be downloaded from
the [releases page](https://github.com/Exlll/ConfigLib/releases) where you can identify them by
their `-paper-`, `-waterfall-`, and `-velocity-` infix and `-all` suffix. Other than that, the
plugin versions currently don't add any additional functionality. If you use these versions, don't
forget to add them as a dependency in the `plugin.yml` (for Paper and Waterfall) or to the
dependencies array (for Velocity) of your own plugin.
Alternatively, if you don't want to use an extra plugin, you can shade the `-core` version and the
YAML parser yourself.
#### Maven
```xml
```xml
<repository>
<id>de.exlll</id>
<url>https://maven.pkg.github.com/Exlll/ConfigLib</url>
</repository>
<!-- for Bukkit plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bukkit</artifactId>
<version>2.2.0</version>
</dependency>
<!-- for Bungee plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bungee</artifactId>
<version>2.2.0</version>
<artifactId>configlib-core</artifactId>
<version>3.0.0</version>
</dependency>
```
#### Gradle
```groovy
repositories { maven { url 'https://maven.pkg.github.com/Exlll/ConfigLib' } }
dependencies {
// for Bukkit plugins
implementation group: 'de.exlll', name: 'configlib-bukkit', version: '2.2.0'
// for Bungee plugins
implementation group: 'de.exlll', name: 'configlib-bungee', version: '2.2.0'
}
dependencies { implementation 'de.exlll:configlib-core:3.0.0' }
```
```kotlin
repositories { maven { url = uri("https://maven.pkg.github.com/Exlll/ConfigLib") } }
dependencies {
// for Bukkit plugins
implementation("de.exlll:configlib-bukkit:2.2.0")
// for Bungee plugins
implementation("de.exlll:configlib-bungee:2.2.0")
}
```
dependencies { implementation("de.exlll:configlib-core:3.0.0") }
```
## Future work
This section contains ideas for upcoming features. If you want any of these to happen any time soon,
please [open an issue](https://github.com/Exlll/ConfigLib/issues/new) where we can discuss the
details.
- Optional fields
- Post load / pre save hooks
- TOML support
- Change the order of fields in parent/child class scenarios
- Recursive definitions

@ -1,77 +1,4 @@
plugins {
java
idea
`maven-publish`
}
allprojects {
group = "de.exlll"
version = "2.2.0"
}
subprojects {
apply(plugin = "java")
apply(plugin = "idea")
apply(plugin = "maven-publish")
java.sourceCompatibility = JavaVersion.VERSION_1_8
java.targetCompatibility = JavaVersion.VERSION_1_8
repositories {
mavenCentral()
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:5.0.3")
testImplementation("org.junit.platform:junit-platform-runner:1.0.3")
testImplementation("org.junit.platform:junit-platform-suite-api:1.0.3")
testImplementation("org.hamcrest:hamcrest-all:1.3")
testImplementation("com.google.jimfs:jimfs:1.1")
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Exlll/ConfigLib")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
publications {
register<MavenPublication>("gpr") {
from(components["java"])
}
}
}
}
}
project(":configlib-core") {
dependencies {
implementation("org.yaml:snakeyaml:1.20")
}
}
project(":configlib-bukkit") {
repositories {
maven(url = "https://hub.spigotmc.org/nexus/content/repositories/snapshots/")
}
dependencies {
implementation(project(":configlib-core"))
implementation("org.bukkit:bukkit:1.12.2-R0.1-SNAPSHOT")
}
tasks.jar { from(project(":configlib-core").sourceSets["main"].output) }
}
project(":configlib-bungee") {
repositories {
maven(url = "https://oss.sonatype.org/content/repositories/snapshots")
}
dependencies {
implementation(project(":configlib-core"))
implementation("net.md-5:bungeecord-api:1.12-SNAPSHOT")
}
tasks.jar { from(project(":configlib-core").sourceSets["main"].output) }
version = "3.0.0"
}

@ -0,0 +1,12 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2")
}

@ -0,0 +1,91 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
java
idea
`maven-publish`
id("com.github.johnrengelman.shadow")
}
val shade = configurations.create("shade")
configurations {
compileClasspath.get().extendsFrom(shade)
}
val shadowJarTask = tasks.getByName<ShadowJar>("shadowJar") {
configurations = listOf(shade)
relocate("org.snakeyaml.engine", "de.exlll.configlib.org.snakeyaml.engine")
}
val coreProjectCheckTask = project(":configlib-core").tasks.getByName("check");
shadowJarTask.dependsOn(
tasks.named("check"),
coreProjectCheckTask
)
tasks.getByName("build").dependsOn(coreProjectCheckTask)
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
withJavadocJar()
withSourcesJar()
}
tasks.getByName<Test>("test") {
useJUnitPlatform()
}
repositories {
mavenCentral()
maven(url = "https://papermc.io/repo/repository/maven-public/")
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testImplementation("org.junit.platform:junit-platform-runner:1.8.2")
testImplementation("org.junit.platform:junit-platform-suite-api:1.8.2")
testImplementation("org.mockito:mockito-inline:4.2.0")
testImplementation("org.mockito:mockito-junit-jupiter:4.2.0")
testImplementation("org.hamcrest:hamcrest-all:1.3")
testImplementation("com.google.jimfs:jimfs:1.2")
}
val javaComponent = components["java"] as AdhocComponentWithVariants
javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) {
skip()
}
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/Exlll/ConfigLib")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
val moduleId = project.name.split("-")[1].toLowerCase()
val publicationName = moduleId.capitalize()
publications {
register<MavenPublication>(publicationName) {
from(components["java"])
}
}
}
idea {
module {
isDownloadJavadoc = true
isDownloadSources = true
}
}

@ -0,0 +1,8 @@
plugins {
`java-config`
}
dependencies {
shade("org.snakeyaml:snakeyaml-engine:2.3")
implementation("org.snakeyaml:snakeyaml-engine:2.3")
}

@ -0,0 +1,21 @@
package de.exlll.configlib;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated element is saved together with explanatory
* comments describing it.
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Comment {
/**
* Returns the comments of the annotated field.
*
* @return field comments
*/
String[] value();
}

@ -0,0 +1,29 @@
package de.exlll.configlib;
import java.util.List;
/**
* Holds the comments of a field as well as a list of field names. The list of
* field names contains the names of all fields which led to this field from the
* root of the configuration object. For example, for the following situation the
* {@code CommentNode} of the field {@code fn2} would hold {@code comments} and
* {@code fieldNames} lists that contain the values {@code ["Hello World"]} and
* {@code ["fn0", "fn1", "fn2"]}, respectively.
* <pre>
* class A {
* B fn0 = new B();
* }
* class B {
* C fn1 = new C();
* }
* class C {
* {@code @Comment("Hello world")}
* int fn2;
* }
* </pre>
*
* @param comments
* @param fieldNames
*/
record CommentNode(List<String> comments, List<String> fieldNames) {}

@ -0,0 +1,83 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.*;
final class CommentNodeExtractor {
private final FieldFilter fieldFilter;
private final FieldFormatter fieldFormatter;
private final boolean outputNull;
CommentNodeExtractor(ConfigurationProperties properties) {
this.fieldFilter = Validator.requireNonNull(properties.getFieldFilter(), "field filter");
this.fieldFormatter = Validator.requireNonNull(properties.getFieldFormatter(), "field formatter");
this.outputNull = properties.outputNulls();
}
private record State(Iterator<Field> iterator, Object configuration) {}
/**
* Extracts {@code CommentNode}s of the given configuration in a DFS manner.
* The nodes are returned in the order in which they were found.
*
* @param configuration the configuration from which the nodes are extracted
* @return the nodes in the order in which they are found
* @throws IllegalArgumentException if {@code configuration} is not a configuration
* @throws NullPointerException if {@code configuration} is null
*/
public Queue<CommentNode> extractCommentNodes(final Object configuration) {
Validator.requireConfiguration(configuration.getClass());
final Queue<CommentNode> result = new ArrayDeque<>();
final var fnameStack = new ArrayDeque<>(List.of(""));
final var stateStack = new ArrayDeque<>(List.of(
new State(configurationFields(configuration), configuration)
));
State state;
while (!stateStack.isEmpty()) {
state = stateStack.removeLast();
fnameStack.removeLast();
while (state.iterator.hasNext()) {
final var field = state.iterator.next();
final var value = Reflect.getValue(field, state.configuration);
if ((value == null) && !outputNull)
continue;
final var commentNode = createNodeIfFieldHasComment(field, fnameStack);
commentNode.ifPresent(result::add);
if ((value == null) || !Reflect.isConfiguration(field.getType()))
continue;
stateStack.addLast(new State(state.iterator, state.configuration));
fnameStack.addLast(fieldFormatter.format(field));
state = new State(configurationFields(value), value);
}
}
return result;
}
private Optional<CommentNode> createNodeIfFieldHasComment(
Field field,
Deque<String> fileNameStack
) {
if (field.isAnnotationPresent(Comment.class)) {
final var comments = field.getAnnotation(Comment.class).value();
final var fieldName = fieldFormatter.format(field);
final var fieldNames = new ArrayList<>(fileNameStack.stream().toList());
fieldNames.add(fieldName);
final var result = new CommentNode(Arrays.asList(comments), fieldNames);
return Optional.of(result);
}
return Optional.empty();
}
private Iterator<Field> configurationFields(Object configuration) {
return FieldExtractors.CONFIGURATION.extract(configuration.getClass())
.filter(fieldFilter)
.iterator();
}
}

@ -0,0 +1,13 @@
package de.exlll.configlib;
import java.lang.annotation.*;
/**
* Indicates the annotated type is a configuration.
* <p>
* Configuration classes must have a no-args constructor.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface Configuration {}

@ -1,9 +1,7 @@
package de.exlll.configlib;
/**
* Signals that an error occurred during the (de-)serialization of a configuration.
* <p>
* The cause of this exception is most likely some misconfiguration.
* Signals that an error occurred during the serialization or deserialization of a configuration.
*/
public final class ConfigurationException extends RuntimeException {
ConfigurationException(String message) {

@ -0,0 +1,223 @@
package de.exlll.configlib;
import java.util.HashMap;
import java.util.Map;
import static de.exlll.configlib.Validator.requireNonNull;
/**
* A collection of values used to configure the serialization of configurations.
*/
class ConfigurationProperties {
private final Map<Class<?>, Serializer<?, ?>> serializersByType;
private final FieldFormatter formatter;
private final FieldFilter filter;
private final boolean outputNulls;
private final boolean inputNulls;
private final boolean serializeSetsAsLists;
/**
* Constructs a new instance of this class with values taken from the given builder.
*
* @param builder the builder used to initialize the fields of this class
* @throws NullPointerException if the builder or any of its values is null
*/
protected ConfigurationProperties(Builder<?> builder) {
this.serializersByType = Map.copyOf(builder.serializersByType);
this.formatter = requireNonNull(builder.formatter, "field formatter");
this.filter = requireNonNull(builder.filter, "field filter");
this.outputNulls = builder.outputNulls;
this.inputNulls = builder.inputNulls;
this.serializeSetsAsLists = builder.serializeSetsAsLists;
}
/**
* Constructs a new {@code Builder} with default values.
*
* @return newly constructed {@code Builder}
*/
public static Builder<?> newBuilder() {
return new BuilderImpl();
}
private static final class BuilderImpl extends Builder<BuilderImpl> {
@Override
protected BuilderImpl getThis() {return this;}
@Override
public ConfigurationProperties build() {return new ConfigurationProperties(this);}
}
/**
* A builder class for constructing {@code ConfigurationProperties}.
*
* @param <B> the type of builder
*/
public static abstract class Builder<B extends Builder<B>> {
private final Map<Class<?>, Serializer<?, ?>> serializersByType = new HashMap<>();
/* change setter JavaDoc if default values are changed */
private FieldFormatter formatter = FieldFormatters.IDENTITY;
private FieldFilter filter = FieldFilters.DEFAULT;
private boolean outputNulls = false;
private boolean inputNulls = false;
private boolean serializeSetsAsLists = true;
protected Builder() {}
/**
* Sets the field filter. The given filter is applied in addition to and
* after the default filter.
*
* @param filter the filter
* @return this builder
* @throws NullPointerException if {@code filter} is null
*/
public final B setFieldFilter(FieldFilter filter) {
this.filter = requireNonNull(filter, "field filter");
return getThis();
}
/**
* Sets the field formatter.
* <p>
* The default value is a formatter that returns the name of the field.
*
* @param formatter the formatter
* @return this builder
* @throws NullPointerException if {@code formatter} is null
*/
public final B setFieldFormatter(FieldFormatter formatter) {
this.formatter = requireNonNull(formatter, "field formatter");
return getThis();
}
/**
* Adds a serializer for the given type. If this library already provides a serializer
* for the given type (e.g. {@code BigInteger}, {@code LocalDate}, etc.) the serializer
* added by this method takes precedence.
*
* @param serializedType the class of the type that is serialized
* @param serializer the serializer
* @param <T> the type that is serialized
* @return this builder
* @throws NullPointerException if any argument is null
*/
public final <T> B addSerializer(Class<T> serializedType, Serializer<T, ?> serializer) {
requireNonNull(serializedType, "serialized type");
requireNonNull(serializer, "serializer");
serializersByType.put(serializedType, serializer);
return getThis();
}
/**
* Sets whether fields or collection elements whose value is null should be output
* while serializing the configuration.
* <p>
* The default value is {@code false}.
*
* @param outputNulls whether to output null values
* @return this builder
*/
public final B outputNulls(boolean outputNulls) {
this.outputNulls = outputNulls;
return getThis();
}
/**
* Sets whether fields or collection elements should allow null values to bet set
* while deserializing the configuration.
* <p>
* The default value is {@code false}.
*
* @param inputNulls whether to input null values
* @return this builder
*/
public final B inputNulls(boolean inputNulls) {
this.inputNulls = inputNulls;
return getThis();
}
/**
* Sets whether sets should be serialized as lists.
* <p>
* The default value is {@code true}.
*
* @param serializeSetsAsLists whether to serialize sets as lists
* @return this builder
*/
final B serializeSetsAsLists(boolean serializeSetsAsLists) {
this.serializeSetsAsLists = serializeSetsAsLists;
return getThis();
}
/**
* Builds a {@code ConfigurationProperties} instance.
*
* @return newly constructed {@code ConfigurationProperties}
*/
public abstract ConfigurationProperties build();
/**
* Returns this builder.
*
* @return this builder
*/
protected abstract B getThis();
}
/**
* Returns the field filter used to filter the fields of a configuration.
*
* @return the field filter
*/
public final FieldFilter getFieldFilter() {
return filter;
}
/**
* Returns the field formatter used to format the fields of a configuration.
*
* @return the formatter
*/
public final FieldFormatter getFieldFormatter() {
return formatter;
}
/**
* Returns an unmodifiable map of serializers by type. The serializers returned by this
* method take precedence over any default serializers provided by this library.
*
* @return serializers by type
*/
public final Map<Class<?>, Serializer<?, ?>> getSerializers() {
return serializersByType;
}
/**
* Returns whether null values should be output.
*
* @return whether to output null values
*/
public final boolean outputNulls() {
return outputNulls;
}
/**
* Returns whether null values should be allowed as input.
*
* @return whether to input null values
*/
public final boolean inputNulls() {
return inputNulls;
}
/**
* Returns whether sets should be serialized as lists.
*
* @return whether to serialize sets as lists
*/
final boolean serializeSetsAsLists() {
return serializeSetsAsLists;
}
}

@ -0,0 +1,139 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
final class ConfigurationSerializer<T> implements Serializer<T, Map<?, ?>> {
private final Class<T> configurationType;
private final ConfigurationProperties properties;
private final Map<String, Serializer<?, ?>> serializers;
ConfigurationSerializer(Class<T> configurationType, ConfigurationProperties properties) {
this.configurationType = Validator.requireConfiguration(configurationType);
this.properties = properties;
this.serializers = buildSerializerMap();
requireSerializableFields();
}
private void requireSerializableFields() {
if (serializers.isEmpty()) {
String msg = "Configuration class '" + configurationType.getSimpleName() + "' " +
"does not contain any (de-)serializable fields.";
throw new ConfigurationException(msg);
}
}
@Override
public Map<?, ?> serialize(T element) {
final Map<String, Object> result = new LinkedHashMap<>();
for (final Field field : filterFields()) {
final Object fieldValue = Reflect.getValue(field, element);
if ((fieldValue == null) && !properties.outputNulls())
continue;
final Object serializedValue = serialize(field, fieldValue);
final String formattedField = properties.getFieldFormatter().format(field);
result.put(formattedField, serializedValue);
}
return result;
}
private Object serialize(Field field, Object fieldValue) {
// The following cast won't cause a ClassCastException because we select the
// serializers based on the field type.
@SuppressWarnings("unchecked")
final Serializer<Object, Object> serializer =
(Serializer<Object, Object>) serializers.get(field.getName());
return (fieldValue != null) ? serializer.serialize(fieldValue) : null;
}
@Override
public T deserialize(Map<?, ?> element) {
final T result = Reflect.newInstance(configurationType);
for (final Field field : filterFields()) {
final String formattedField = properties.getFieldFormatter().format(field);
if (!element.containsKey(formattedField))
continue;
final Object value = element.get(formattedField);
if (value == null && properties.inputNulls()) {
requireNonPrimitiveFieldType(field);
Reflect.setValue(field, result, null);
} else if (value != null) {
final Object deserialized = deserialize(field, value);
Reflect.setValue(field, result, deserialized);
}
}
return result;
}
private Object deserialize(Field field, Object value) {
// This unchecked cast leads to an exception if the type of the object which
// is deserialized is not a subtype of the type the deserializer expects.
@SuppressWarnings("unchecked")
final Serializer<Object, Object> serializer =
(Serializer<Object, Object>) serializers.get(field.getName());
final Object deserialized;
try {
deserialized = serializer.deserialize(value);
} catch (ClassCastException e) {
String msg = baseDeserializeExceptionMessage(field, value) + "\n" +
"The type of the object to be deserialized does not " +
"match the type the deserializer expects.";
throw new ConfigurationException(msg, e);
} catch (RuntimeException e) {
String msg = baseDeserializeExceptionMessage(field, value);
throw new ConfigurationException(msg, e);
}
return deserialized;
}
private static String baseDeserializeExceptionMessage(Field field, Object value) {
return "Deserialization of value '" + value + "' with type " + value.getClass() + " " +
"for field " + field + " failed.";
}
private static void requireNonPrimitiveFieldType(Field field) {
if (field.getType().isPrimitive()) {
String msg = "Cannot set " + field + " to null value.\n" +
"Primitive types cannot be assigned null.";
throw new ConfigurationException(msg);
}
}
private List<Field> filterFields() {
return FieldExtractors.CONFIGURATION.extract(configurationType)
.filter(properties.getFieldFilter())
.toList();
}
Map<String, Serializer<?, ?>> buildSerializerMap() {
SerializerSelector selector = new SerializerSelector(properties);
try {
return filterFields().stream()
.collect(Collectors.toMap(
Field::getName,
field -> selector.select(field.getGenericType()),
(serializer1, serializer2) -> serializer2
));
} catch (StackOverflowError error) {
String msg = "Recursive type definitions are not supported.";
throw new ConfigurationException(msg, error);
}
}
Class<T> getConfigurationType() {
return configurationType;
}
}

@ -0,0 +1,236 @@
package de.exlll.configlib;
import java.nio.file.Path;
import java.util.function.Consumer;
/**
* This class contains convenience methods for loading, saving, and updating configurations.
*/
public final class Configurations {
private Configurations() {}
/**
* Loads a configuration of the given type from the specified YAML file using a
* {@code YamlConfigurationProperties} object with default values.
*
* @param configurationFile the file the configuration is loaded from
* @param configurationType the type of configuration
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws IllegalArgumentException if the file does not exist or is not a regular file
* @throws NullPointerException if any parameter is null
* @throws RuntimeException if reading the configuration throws an exception
* @see YamlConfigurationStore#load(Path)
*/
public static <T> T loadYamlConfiguration(
Path configurationFile,
Class<T> configurationType
) {
final var properties = YamlConfigurationProperties.newBuilder().build();
return loadYamlConfiguration(configurationFile, configurationType, properties);
}
/**
* Loads a configuration of the given type from the specified YAML file using a
* {@code YamlConfigurationProperties} object that is built by a builder. The builder is
* initialized with default values and can be configured by the {@code propertiesConfigurer}.
*
* @param configurationFile the file the configuration is loaded from
* @param configurationType the type of configuration
* @param propertiesConfigurer the consumer used to configure the builder
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws IllegalArgumentException if the file does not exist or is not a regular file
* @throws NullPointerException if any parameter is null
* @throws RuntimeException if reading the configuration throws an exception
* @see YamlConfigurationStore#load(Path)
*/
public static <T> T loadYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) {
final var builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder);
return loadYamlConfiguration(configurationFile, configurationType, builder.build());
}
/**
* Loads a configuration of the given type from the specified YAML file using the given
* {@code YamlConfigurationProperties} object.
*
* @param configurationFile the file the configuration is loaded from
* @param configurationType the type of configuration
* @param properties the configuration properties
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws IllegalArgumentException if the file does not exist or is not a regular file
* @throws NullPointerException if any parameter is null
* @throws RuntimeException if reading the configuration throws an exception
* @see YamlConfigurationStore#load(Path)
*/
public static <T> T loadYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
YamlConfigurationProperties properties
) {
final var store = new YamlConfigurationStore<>(configurationType, properties);
return store.load(configurationFile);
}
/**
* Updates a YAML configuration file with a configuration of the given type using a
* {@code YamlConfigurationProperties} object with default values.
* <p>
* See {@link YamlConfigurationStore#save(Object, Path)} for an explanation of how the update is
* done.
*
* @param configurationFile the configuration file that is updated
* @param configurationType the type of configuration
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if loading or saving the configuration throws an exception
* @see YamlConfigurationStore#update(Path)
*/
public static <T> T updateYamlConfiguration(
Path configurationFile,
Class<T> configurationType
) {
final var properties = YamlConfigurationProperties.newBuilder().build();
return updateYamlConfiguration(configurationFile, configurationType, properties);
}
/**
* Updates a YAML configuration file with a configuration of the given type using a
* {@code YamlConfigurationProperties} object that is built by a builder. The builder is
* initialized with default values and can be configured by the {@code propertiesConfigurer}.
* <p>
* See {@link YamlConfigurationStore#save(Object, Path)} for an explanation of how the update is
* done.
*
* @param configurationFile the configuration file that is updated
* @param configurationType the type of configuration
* @param propertiesConfigurer the consumer used to configure the builder
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if loading or saving the configuration throws an exception
* @see YamlConfigurationStore#update(Path)
*/
public static <T> T updateYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) {
final var builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder);
return updateYamlConfiguration(configurationFile, configurationType, builder.build());
}
/**
* Updates a YAML configuration file with a configuration of the given type using the given
* {@code YamlConfigurationProperties} object.
* <p>
* See {@link YamlConfigurationStore#save(Object, Path)} for an explanation of how the update is
* done.
*
* @param configurationFile the configuration file that is updated
* @param configurationType the type of configuration
* @param properties the configuration properties
* @param <T> the configuration type
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if loading or saving the configuration throws an exception
* @see YamlConfigurationStore#update(Path)
*/
public static <T> T updateYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
YamlConfigurationProperties properties
) {
final var store = new YamlConfigurationStore<>(configurationType, properties);
return store.update(configurationFile);
}
/**
* Saves a configuration of the given type to the specified YAML file using a
* {@code YamlConfigurationProperties} object with default values.
*
* @param configuration the configuration that is saved
* @param configurationType the type of configuration
* @param configurationFile the file the configuration is saved to
* @param <T> the configuration type
* @throws ConfigurationException if the configuration contains invalid values or
* cannot be serialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if writing the configuration throws an exception
* @see YamlConfigurationStore#save(Object, Path)
*/
public static <T> void saveYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
T configuration
) {
final var properties = YamlConfigurationProperties.newBuilder().build();
saveYamlConfiguration(configurationFile, configurationType, configuration, properties);
}
/**
* Saves a configuration of the given type to the specified YAML file using a
* {@code YamlConfigurationProperties} object that is built by a builder. The builder is
* initialized with default values and can be configured by the {@code propertiesConfigurer}.
*
* @param configuration the configuration that is saved
* @param configurationType the type of configuration
* @param configurationFile the file the configuration is saved to
* @param propertiesConfigurer the consumer used to configure the builder
* @param <T> the configuration type
* @throws ConfigurationException if the configuration contains invalid values or
* cannot be serialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if writing the configuration throws an exception
* @see YamlConfigurationStore#save(Object, Path)
*/
public static <T> void saveYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
T configuration,
Consumer<YamlConfigurationProperties.Builder<?>> propertiesConfigurer
) {
final var builder = YamlConfigurationProperties.newBuilder();
propertiesConfigurer.accept(builder);
saveYamlConfiguration(configurationFile, configurationType, configuration, builder.build());
}
/**
* Saves a configuration of the given type to the specified YAML file using the given
* {@code YamlConfigurationProperties} object.
*
* @param configuration the configuration that is saved
* @param configurationType the type of configuration
* @param configurationFile the file the configuration is saved to
* @param properties the configuration properties
* @param <T> the configuration type
* @throws ConfigurationException if the configuration contains invalid values or
* cannot be serialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if writing the configuration throws an exception
* @see YamlConfigurationStore#save(Object, Path)
*/
public static <T> void saveYamlConfiguration(
Path configurationFile,
Class<T> configurationType,
T configuration,
YamlConfigurationProperties properties
) {
final var store = new YamlConfigurationStore<>(configurationType, properties);
store.save(configuration, configurationFile);
}
}

@ -0,0 +1,25 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Implementations of this interface extract the fields of classes.
*/
@FunctionalInterface
interface FieldExtractor extends Function<Class<?>, Stream<Field>> {
/**
* Extracts the fields of a class.
*
* @param cls the class
* @return a stream of fields
* @throws NullPointerException if {@code cls} is null
*/
Stream<Field> extract(Class<?> cls);
@Override
default Stream<Field> apply(Class<?> cls) {
return extract(cls);
}
}

@ -0,0 +1,59 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;
enum FieldExtractors implements FieldExtractor {
/**
* Extracts the declared fields of the given configuration and all its super classes up to the
* first class that is not a configuration.
* <p>
* The order of the fields is reversed such that the field of super classes are listed first.
*/
CONFIGURATION {
@Override
public Stream<Field> extract(Class<?> cls) {
Validator.requireNonNull(cls, "configuration class");
Validator.requireConfiguration(cls);
List<Class<?>> classes = extractClassesWhile(cls, Reflect::isConfiguration);
List<Field> fields = classes.stream()
.flatMap(c -> Arrays.stream(c.getDeclaredFields()))
.filter(FieldFilters.DEFAULT)
.toList();
requireNoShadowing(fields);
return fields.stream();
}
};
private static void requireNoShadowing(List<Field> fields) {
Map<String, Class<?>> map = new LinkedHashMap<>();
for (Field field : fields) {
var fieldName = field.getName();
var fieldClass = field.getDeclaringClass();
if (map.containsKey(fieldName)) {
Class<?> superClass = map.get(fieldName);
String msg = "Shadowing of fields is not supported. Field '" + fieldName + "' " +
"of class " + fieldClass.getSimpleName() + " shadows field '" +
fieldName + "' of class " + superClass.getSimpleName() + ".";
throw new ConfigurationException(msg);
}
map.put(fieldName, fieldClass);
}
}
private static List<Class<?>> extractClassesWhile(Class<?> cls, Predicate<Class<?>> condition) {
List<Class<?>> classes = new ArrayList<>();
Class<?> current = cls;
while (condition.test(current)) {
classes.add(current);
current = current.getSuperclass();
}
Collections.reverse(classes); // we want the fields of the super classes to come first
return classes;
}
}

@ -0,0 +1,15 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.function.Predicate;
/**
* Implementations of this interface test fields for specific conditions.
*/
@FunctionalInterface
public interface FieldFilter extends Predicate<Field> {
@Override
default FieldFilter and(Predicate<? super Field> other) {
return field -> test(field) && other.test(field);
}
}

@ -0,0 +1,27 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
enum FieldFilters implements FieldFilter {
/**
* The default {@code FieldFilter} that rejects fields that are final, static,
* synthetic, transient, or annotated with {@code @Ignore}.
*/
DEFAULT {
@Override
public boolean test(Field field) {
Validator.requireNonNull(field, "field");
if (field.isSynthetic())
return false;
if (Reflect.isIgnored(field))
return false;
int modifiers = field.getModifiers();
return !Modifier.isFinal(modifiers) &&
!Modifier.isStatic(modifiers) &&
!Modifier.isTransient(modifiers);
}
}
}

@ -0,0 +1,24 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.function.Function;
/**
* Implementations of this interface format the names of fields.
*/
@FunctionalInterface
public interface FieldFormatter extends Function<Field, String> {
/**
* Formats the name of the given field.
*
* @param field the field
* @return formatted field name
* @throws NullPointerException if {@code field} is null
*/
String format(Field field);
@Override
default String apply(Field field) {
return format(field);
}
}

@ -0,0 +1,59 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
/**
* This class contains instances of ready-to-use {@code FieldFormatter}s.
*/
public enum FieldFormatters implements FieldFormatter {
/**
* A {@code FieldFormatter} that returns the name of the field.
*/
IDENTITY {
@Override
public String format(Field field) {
return field.getName();
}
},
/**
* A {@code FieldFormatter} that transforms <i>camelCase</i> to
* <i>lower_underscore</i>.
* <p>
* For example, <i>myPrivateField</i> becomes <i>my_private_field</i>.
*/
LOWER_UNDERSCORE {
@Override
public String format(Field field) {
String fn = field.getName();
StringBuilder builder = new StringBuilder(fn.length());
for (char c : fn.toCharArray()) {
if (Character.isUpperCase(c)) {
char lower = Character.toLowerCase(c);
builder.append('_').append(lower);
} else builder.append(c);
}
return builder.toString();
}
},
/**
* A {@code FieldFormatter} that transforms <i>camelCase</i> to
* <i>UPPER_UNDERSCORE</i>.
* <p>
* For example, <i>myPrivateField</i> becomes <i>MY_PRIVATE_FIELD</i>.
*/
UPPER_UNDERSCORE {
@Override
public String format(Field field) {
String fn = field.getName();
StringBuilder builder = new StringBuilder(fn.length());
for (char c : fn.toCharArray()) {
if (Character.isLowerCase(c)) {
builder.append(Character.toUpperCase(c));
} else if (Character.isUpperCase(c)) {
builder.append('_').append(c);
} else builder.append(c);
}
return builder.toString();
}
}
}

@ -0,0 +1,137 @@
package de.exlll.configlib;
/**
* An extension of the {@code ConfigurationProperties} class that allows configuring properties
* that are more specific to files.
*/
public class FileConfigurationProperties extends ConfigurationProperties {
private final String header;
private final String footer;
private final boolean createParentDirectories;
/**
* Constructs a new instance of this class with values taken from the given builder.
*
* @param builder the builder used to initialize the fields of this class
* @throws NullPointerException if the builder or any of its values is null
*/
protected FileConfigurationProperties(Builder<?> builder) {
super(builder);
this.header = builder.header;
this.footer = builder.footer;
this.createParentDirectories = builder.createParentDirectories;
}
/**
* Constructs a new {@code Builder} with default values.
*
* @return newly constructed {@code Builder}
*/
public static Builder<?> newBuilder() {
return new BuilderImpl();
}
private static final class BuilderImpl extends Builder<BuilderImpl> {
@Override
protected BuilderImpl getThis() {return this;}
@Override
public FileConfigurationProperties build() {return new FileConfigurationProperties(this);}
}
/**
* A builder class for constructing {@code FileConfigurationProperties}.
*
* @param <B> the type of builder
*/
public static abstract class Builder<B extends Builder<B>>
extends ConfigurationProperties.Builder<B> {
private String header = null;
private String footer = null;
private boolean createParentDirectories = true;
/**
* A constructor that can be overridden by subclasses.
*/
protected Builder() {}
/**
* Sets the header. The header is written as a comment before the actual configuration.
* If the header is set to null (the default), nothing is written.
*
* @param header the header
* @return this builder
*/
public final B header(String header) {
this.header = header;
return getThis();
}
/**
* Sets the footer. The footer is written as a comment after the actual configuration.
* If the footer is set to null (the default), nothing is written.
*
* @param footer the footer
* @return this builder
*/
public final B footer(String footer) {
this.footer = footer;
return getThis();
}
/**
* Sets whether parent directories of a configuration file should be created.
* <p>
* The default value is {@code true}.
*
* @param createParentDirectories whether to create parent directories
* @return this builder
*/
public final B createParentDirectories(boolean createParentDirectories) {
this.createParentDirectories = createParentDirectories;
return getThis();
}
/**
* Builds a {@code ConfigurationProperties} instance.
*
* @return newly constructed {@code ConfigurationProperties}
*/
public abstract FileConfigurationProperties build();
/**
* Returns this builder.
*
* @return this builder
*/
protected abstract B getThis();
}
/**
* Returns the header.
*
* @return the header
*/
public final String getHeader() {
return header;
}
/**
* Returns the footer.
*
* @return the footer
*/
public final String getFooter() {
return footer;
}
/**
* Returns whether to create parent directories.
*
* @return whether to create parent directories
*/
public final boolean createParentDirectories() {
return createParentDirectories;
}
}

@ -0,0 +1,49 @@
package de.exlll.configlib;
import java.nio.file.Path;
/**
* Instances of this class save and load configurations using files.
*
* @param <T> the configuration type
*/
public interface FileConfigurationStore<T> {
/**
* Saves a configuration instance to the given file.
*
* @param configuration the configuration
* @param configurationFile the file the configuration is saved to
* @throws ConfigurationException if the configuration contains invalid values or
* cannot be serialized
* @throws NullPointerException if any argument is null
* @throws RuntimeException if writing the configuration throws an exception
*/
void save(T configuration, Path configurationFile);
/**
* Loads a configuration from the given file.
*
* @param configurationFile the file the configuration is loaded from
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws IllegalArgumentException if the file does not exist or is not a regular file
* @throws NullPointerException if {@code configurationFile} is null
* @throws RuntimeException if reading the configuration throws an exception
*/
T load(Path configurationFile);
/**
* Updates the configuration file. If the file does not exist, it is created and populated
* with the default values with which the fields of the configuration have been initialized.
* Otherwise, a new configuration instance is created, initialized with the values taken from
* the configuration file, and immediately saved to reflect possible changes of the
* configuration type.
*
* @param configurationFile the configuration file that is updated
* @return a newly created configuration initialized with values taken from the configuration file
* @throws ConfigurationException if the configuration cannot be deserialized
* @throws NullPointerException if {@code configurationFile} is null
* @throws RuntimeException if loading or saving the configuration throws an exception
*/
T update(Path configurationFile);
}

@ -0,0 +1,13 @@
package de.exlll.configlib;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates that the annotated field should not be serialized.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Ignore {}

@ -0,0 +1,110 @@
package de.exlll.configlib;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.Set;
final class Reflect {
private Reflect() {}
static <T> T newInstance(Class<T> cls) {
try {
Constructor<T> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (NoSuchMethodException e) {
String msg = "Class " + cls.getSimpleName() + " doesn't have a " +
"no-args constructor.";
throw new RuntimeException(msg, e);
} catch (IllegalAccessException e) {
/* This exception should not be thrown because
* we set the constructor to be accessible. */
String msg = "No-args constructor of class " + cls.getSimpleName() +
" not accessible.";
throw new RuntimeException(msg, e);
} catch (InstantiationException e) {
String msg = "Class " + cls.getSimpleName() + " is not instantiable.";
throw new RuntimeException(msg, e);
} catch (InvocationTargetException e) {
String msg = "Constructor of class " + cls.getSimpleName() + " threw an exception.";
throw new RuntimeException(msg, e);
}
}
static <T> T[] newArray(Class<T> componentType, int length) {
// The following cast won't fail because we just created an array of that type
@SuppressWarnings("unchecked")
T[] array = (T[]) Array.newInstance(componentType, length);
return array;
}
static Object getValue(Field field, Object instance) {
try {
field.setAccessible(true);
return field.get(instance);
} catch (IllegalAccessException e) {
/* This exception should not be thrown because
* we set the field to be accessible. */
String msg = "Illegal access of field '" + field + "' " +
"on object " + instance + ".";
throw new RuntimeException(msg, e);
}
}
static void setValue(Field field, Object instance, Object value) {
try {
field.setAccessible(true);
field.set(instance, value);
} catch (IllegalAccessException e) {
/* This exception should not be thrown because
* we set the field to be accessible. */
String msg = "Illegal access of field '" + field + "' " +
"on object " + instance + ".";
throw new RuntimeException(msg, e);
}
}
static boolean isIntegerType(Class<?> cls) {
return (cls == byte.class) || (cls == Byte.class) ||
(cls == short.class) || (cls == Short.class) ||
(cls == int.class) || (cls == Integer.class) ||
(cls == long.class) || (cls == Long.class);
}
static boolean isFloatingPointType(Class<?> cls) {
return (cls == float.class) || (cls == Float.class) ||
(cls == double.class) || (cls == Double.class);
}
static boolean isEnumType(Class<?> cls) {
return cls.isEnum();
}
static boolean isArrayType(Class<?> cls) {
return cls.isArray();
}
static boolean isListType(Class<?> cls) {
return List.class.isAssignableFrom(cls);
}
static boolean isSetType(Class<?> cls) {
return Set.class.isAssignableFrom(cls);
}
static boolean isMapType(Class<?> cls) {
return Map.class.isAssignableFrom(cls);
}
static boolean isConfiguration(Class<?> cls) {
return cls.getAnnotation(Configuration.class) != null;
}
static boolean isIgnored(Field field) {
return field.getAnnotation(Ignore.class) != null;
}
}

@ -0,0 +1,42 @@
package de.exlll.configlib;
/**
* Implementations of this interface convert instances of type {@code T1} to a serializable type
* {@code T2} and vice versa.
* <p>
* Which types {@code T2} are serializable depends on the underlying storage system. Currently,
* all storage systems support the following types:
* <ul>
* <li>{@code Boolean}</li>
* <li>{@code Long}</li>
* <li>{@code Double}</li>
* <li>{@code String}</li>
* <li>(Nested) {@code List}s of the other types</li>
* <li>(Nested) {@code Set}s of the other types</li>
* <li>(Nested) {@code Map}s of the other types</li>
* </ul>
* <p>
* That means that if you want to support all currently available configuration store formats,
* your {@code Serializer} implementation should convert an object of type {@code T1} into one
* of the seven types listed above.
*
* @param <T1> the type of the objects that should be serialized
* @param <T2> the serializable type
*/
public interface Serializer<T1, T2> {
/**
* Serializes an element of type {@code T1} into an element of type {@code T2}.
*
* @param element the element of type {@code T1} that is serialized
* @return the serialized element of type {@code T2}
*/
T2 serialize(T1 element);
/**
* Deserializes an element of type {@code T2} into an element of type {@code T1}.
*
* @param element the element of type {@code T2} that is deserialized
* @return the deserialized element of type {@code T1}
*/
T1 deserialize(T2 element);
}

@ -0,0 +1,145 @@
package de.exlll.configlib;
import de.exlll.configlib.Serializers.*;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;
final class SerializerSelector {
private static final Map<Class<?>, Serializer<?, ?>> DEFAULT_SERIALIZERS = Map.ofEntries(
Map.entry(boolean.class, new BooleanSerializer()),
Map.entry(Boolean.class, new BooleanSerializer()),
Map.entry(byte.class, new NumberSerializer(byte.class)),
Map.entry(Byte.class, new NumberSerializer(Byte.class)),
Map.entry(short.class, new NumberSerializer(short.class)),
Map.entry(Short.class, new NumberSerializer(Short.class)),
Map.entry(int.class, new NumberSerializer(int.class)),
Map.entry(Integer.class, new NumberSerializer(Integer.class)),
Map.entry(long.class, new NumberSerializer(long.class)),
Map.entry(Long.class, new NumberSerializer(Long.class)),
Map.entry(float.class, new NumberSerializer(float.class)),
Map.entry(Float.class, new NumberSerializer(Float.class)),
Map.entry(double.class, new NumberSerializer(double.class)),
Map.entry(Double.class, new NumberSerializer(Double.class)),
Map.entry(char.class, new CharacterSerializer()),
Map.entry(Character.class, new CharacterSerializer()),
Map.entry(String.class, new StringSerializer()),
Map.entry(BigInteger.class, new BigIntegerSerializer()),
Map.entry(BigDecimal.class, new BigDecimalSerializer()),
Map.entry(LocalDate.class, new LocalDateSerializer()),
Map.entry(LocalTime.class, new LocalTimeSerializer()),
Map.entry(LocalDateTime.class, new LocalDateTimeSerializer())
);
private final ConfigurationProperties properties;
public SerializerSelector(ConfigurationProperties properties) {
this.properties = properties;
}
public Serializer<?, ?> select(Type type) {
if (type instanceof Class<?> cls) {
return selectForClass(cls);
} else if (type instanceof ParameterizedType pType) {
return selectForParameterizedType(pType);
} else if (type instanceof WildcardType) {
String msg = baseExceptionMessage(type) + "Wildcard types cannot be serialized.";
throw new ConfigurationException(msg);
} else if (type instanceof GenericArrayType) {
String msg = baseExceptionMessage(type) + "Generic array types cannot be serialized.";
throw new ConfigurationException(msg);
} else if (type instanceof TypeVariable<?>) {
String msg = baseExceptionMessage(type) + "Type variables cannot be serialized.";
throw new ConfigurationException(msg);
}
// should not happen as we covered all possible types
throw new ConfigurationException(baseExceptionMessage(type));
}
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)) {
// The following cast won't fail because we just checked that it's an enum.
@SuppressWarnings("unchecked")
var enumCls = (Class<? extends Enum<?>>) cls;
return new Serializers.EnumSerializer(enumCls);
}
if (Reflect.isArrayType(cls))
return selectForArray(cls.getComponentType());
if (Reflect.isConfiguration(cls))
return new ConfigurationSerializer<>(cls, properties);
String msg = "Missing serializer for type " + cls + ".\n" +
"Either annotate the type with @Configuration or provide a custom " +
"serializer by adding it to the properties.";
throw new ConfigurationException(msg);
}
private Serializer<?, ?> selectForArray(Class<?> elementType) {
if (elementType == boolean.class) {
return new PrimitiveBooleanArraySerializer();
} else if (elementType == char.class) {
return new PrimitiveCharacterArraySerializer();
} else if (elementType == byte.class) {
return new PrimitiveByteArraySerializer();
} else if (elementType == short.class) {
return new PrimitiveShortArraySerializer();
} else if (elementType == int.class) {
return new PrimitiveIntegerArraySerializer();
} else if (elementType == long.class) {
return new PrimitiveLongArraySerializer();
} else if (elementType == float.class) {
return new PrimitiveFloatArraySerializer();
} else if (elementType == double.class) {
return new PrimitiveDoubleArraySerializer();
}
var elementSerializer = select(elementType);
var inputNulls = properties.inputNulls();
var outputNulls = properties.outputNulls();
return new ArraySerializer<>(elementType, elementSerializer, outputNulls, inputNulls);
}
private Serializer<?, ?> selectForParameterizedType(ParameterizedType type) {
// the raw type returned by Java is always a class
final var rawType = (Class<?>) type.getRawType();
final var typeArgs = type.getActualTypeArguments();
final var inputNulls = properties.inputNulls();
final var outputNulls = properties.outputNulls();
if (Reflect.isListType(rawType)) {
var elementSerializer = select(typeArgs[0]);
return new ListSerializer<>(elementSerializer, outputNulls, inputNulls);
} else if (Reflect.isSetType(rawType)) {
var elementSerializer = select(typeArgs[0]);
return properties.serializeSetsAsLists()
? new SetAsListSerializer<>(elementSerializer, outputNulls, inputNulls)
: new SetSerializer<>(elementSerializer, outputNulls, inputNulls);
} else if (Reflect.isMapType(rawType)) {
if ((typeArgs[0] instanceof Class<?> cls) &&
(DEFAULT_SERIALIZERS.containsKey(cls) ||
Reflect.isEnumType(cls))) {
var keySerializer = select(typeArgs[0]);
var valSerializer = select(typeArgs[1]);
return new MapSerializer<>(keySerializer, valSerializer, outputNulls, inputNulls);
}
String msg = baseExceptionMessage(type) +
"Map keys can only be of simple or enum type.";
throw new ConfigurationException(msg);
}
String msg = baseExceptionMessage(type) +
"Parameterized types other than lists, sets, and maps cannot be serialized.";
throw new ConfigurationException(msg);
}
private String baseExceptionMessage(Type type) {
return "Cannot select serializer for type '" + type + "'.\n";
}
}

@ -0,0 +1,621 @@
package de.exlll.configlib;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.IntFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
final class Serializers {
private Serializers() {}
static final class BooleanSerializer implements Serializer<Boolean, Boolean> {
@Override
public Boolean serialize(Boolean element) {
return element;
}
@Override
public Boolean deserialize(Boolean element) {
return element;
}
}
/**
* Converts a primitive number or its wrapper type to another number type according to the
* following rules:
* <ul>
* <li>
* The {@code serialize} method converts all four integer types to {@code Long}, and
* both floating point types to {@code Double}.
* </li>
* <li>
* The {@code deserialize} method converts a number from any supported type to the
* requested type if the value of the number fits in the range.
* </li>
* </ul>
*/
static final class NumberSerializer implements Serializer<Number, Number> {
private final Class<? extends Number> cls;
public NumberSerializer(Class<? extends Number> cls) {
this.cls = Validator.requireNonNull(cls, "number class");
Validator.requirePrimitiveOrWrapperNumberType(cls);
}
@Override
public Number serialize(Number element) {
if (Reflect.isIntegerType(cls))
return element.longValue();
if (Reflect.isFloatingPointType(cls))
return element.doubleValue();
String msg = "Invalid element '" + element + "' with type " + element.getClass();
throw new ConfigurationException(msg); // should not happen, types checked in ctor
}
@Override
public Number deserialize(Number element) {
if (Reflect.isIntegerType(element.getClass())) {
return deserializeFromIntegerType(element);
}
if (Reflect.isFloatingPointType(element.getClass())) {
return deserializeFromFloatingPointType(element);
}
String clsName = element.getClass().getSimpleName();
String msg = "Cannot deserialize element '" + element + "' of type " + clsName + ".\n" +
"This serializer only supports primitive number types and their wrapper types.";
throw new ConfigurationException(msg);
}
private Number deserializeFromFloatingPointType(Number element) {
double value = element.doubleValue();
if (cls == float.class || cls == Float.class) {
if (Double.isNaN(value))
return Float.NaN;
if (value == Double.POSITIVE_INFINITY)
return Float.POSITIVE_INFINITY;
if (value == Double.NEGATIVE_INFINITY)
return Float.NEGATIVE_INFINITY;
if (value != 0.0)
requireFloatingPointInRange(value);
return element.floatValue();
}
return value;
}
private Number deserializeFromIntegerType(Number element) {
long value = element.longValue();
if (cls == byte.class || cls == Byte.class) {
requireIntegerInRange(value, Byte.MIN_VALUE, Byte.MAX_VALUE);
return element.byteValue();
}
if (cls == short.class || cls == Short.class) {
requireIntegerInRange(value, Short.MIN_VALUE, Short.MAX_VALUE);
return element.shortValue();
}
if (cls == int.class || cls == Integer.class) {
requireIntegerInRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE);
return element.intValue();
}
return value;
}
private void requireIntegerInRange(long value, long low, long high) {
if (value < low || value > high) {
String msg = baseExceptionMessage(value) + "It does not fit into the range " +
"of valid values [" + low + ", " + high + "].";
throw new ConfigurationException(msg);
}
}
private void requireFloatingPointInRange(double value) {
final String clsName = cls.getSimpleName();
if ((value > -Float.MIN_VALUE) && (value < Float.MIN_VALUE)) {
String msg = baseExceptionMessage(value) + "It is smaller than the smallest " +
"possible " + clsName + " value.";
throw new ConfigurationException(msg);
}
if ((value < -Float.MAX_VALUE) || (value > Float.MAX_VALUE)) {
String msg = baseExceptionMessage(value) + "It is larger than the largest " +
"possible " + clsName + " value.";
throw new ConfigurationException(msg);
}
}
private <T extends Number> String baseExceptionMessage(T value) {
String clsName = cls.getSimpleName();
return "Number " + value + " cannot be converted to type " + clsName + ". ";
}
public Class<? extends Number> getNumberClass() {
return cls;
}
}
static final class StringSerializer implements Serializer<String, String> {
@Override
public String serialize(String element) {
return element;
}
@Override
public String deserialize(String element) {
return element;
}
}
static final class CharacterSerializer implements Serializer<Character, String> {
@Override
public String serialize(Character element) {
return element.toString();
}
@Override
public Character deserialize(String element) {
int length = element.length();
if (length == 0) {
String msg = "An empty string cannot be converted to a character.";
throw new ConfigurationException(msg);
}
if (length > 1) {
String msg = "String '" + element + "' is too long to be converted " +
"to a character.";
throw new ConfigurationException(msg);
}
return element.charAt(0);
}
}
static final class BigIntegerSerializer implements Serializer<BigInteger, String> {
@Override
public String serialize(BigInteger element) {
return element.toString();
}
@Override
public BigInteger deserialize(String element) {
return new BigInteger(element);
}
}
static final class BigDecimalSerializer implements Serializer<BigDecimal, String> {
@Override
public String serialize(BigDecimal element) {
return element.toString();
}
@Override
public BigDecimal deserialize(String element) {
return new BigDecimal(element);
}
}
static final class LocalTimeSerializer implements Serializer<LocalTime, String> {
@Override
public String serialize(LocalTime element) {
return element.truncatedTo(ChronoUnit.SECONDS).toString();
}
@Override
public LocalTime deserialize(String element) {
return LocalTime.parse(element);
}
}
static final class LocalDateSerializer implements Serializer<LocalDate, String> {
@Override
public String serialize(LocalDate element) {
return element.toString();
}
@Override
public LocalDate deserialize(String element) {
return LocalDate.parse(element);
}
}
static final class LocalDateTimeSerializer implements Serializer<LocalDateTime, String> {
@Override
public String serialize(LocalDateTime element) {
return element.truncatedTo(ChronoUnit.SECONDS).toString();
}
@Override
public LocalDateTime deserialize(String element) {
return LocalDateTime.parse(element);
}
}
static final class EnumSerializer implements Serializer<Enum<?>, String> {
private final Class<? extends Enum<?>> cls;
public EnumSerializer(Class<? extends Enum<?>> cls) {
this.cls = Validator.requireNonNull(cls, "enum class");
}
@Override
public String serialize(Enum<?> element) {
return element.name();
}
@Override
public Enum<?> deserialize(String element) {
for (Enum<?> constant : cls.getEnumConstants()) {
if (constant.name().equals(element)) {
return constant;
}
}
String msg = createExceptionMessage(element);
throw new ConfigurationException(msg);
}
private String createExceptionMessage(String element) {
String enums = Arrays.stream(cls.getEnumConstants())
.map(Enum::name)
.collect(Collectors.joining(", ", "[", "]"));
return "Enum class " + cls.getSimpleName() + " does not contain enum '" +
element + "'. Valid values are: " + enums;
}
public Class<? extends Enum<?>> getEnumCls() {
return cls;
}
}
static class CollectionSerializer<S, T, L extends Collection<S>, R extends Collection<T>>
implements Serializer<L, R> {
private final Serializer<S, T> serializer;
private final boolean outputNulls;
private final boolean inputNulls;
private final Supplier<L> lSupplier;
private final Supplier<R> rSupplier;
public CollectionSerializer(
Serializer<S, T> serializer,
boolean outputNulls,
boolean inputNulls,
Supplier<L> lSupplier,
Supplier<R> rSupplier
) {
this.serializer = Validator.requireNonNull(serializer, "element serializer");
this.outputNulls = outputNulls;
this.inputNulls = inputNulls;
this.lSupplier = lSupplier;
this.rSupplier = rSupplier;
}
@Override
public final R serialize(L element) {
final Stream<T> stream = outputNulls
? element.stream().map(s -> s == null ? null : serializer.serialize(s))
: element.stream().filter(Objects::nonNull).map(serializer::serialize);
return stream.collect(Collectors.toCollection(rSupplier));
}
@Override
public final L deserialize(R element) {
final Stream<S> stream = inputNulls
? element.stream().map(t -> t == null ? null : serializer.deserialize(t))
: element.stream().filter(Objects::nonNull).map(serializer::deserialize);
return stream.collect(Collectors.toCollection(lSupplier));
}
public final Serializer<S, T> getElementSerializer() {
return serializer;
}
}
static final class ListSerializer<S, T> extends CollectionSerializer<S, T, List<S>, List<T>> {
public ListSerializer(Serializer<S, T> serializer, boolean outputNulls, boolean inputNulls) {
super(serializer, outputNulls, inputNulls, ArrayList::new, ArrayList::new);
}
}
static final class SetSerializer<S, T> extends CollectionSerializer<S, T, Set<S>, Set<T>> {
public SetSerializer(Serializer<S, T> serializer, boolean outputNulls, boolean inputNulls) {
super(serializer, outputNulls, inputNulls, HashSet::new, LinkedHashSet::new);
}
}
static final class SetAsListSerializer<S, T> extends CollectionSerializer<S, T, Set<S>, List<T>> {
public SetAsListSerializer(Serializer<S, T> serializer, boolean outputNulls, boolean inputNulls) {
super(serializer, outputNulls, inputNulls, HashSet::new, ArrayList::new);
}
}
static final class MapSerializer<S1, T1, S2, T2> implements Serializer<Map<S1, S2>, Map<T1, T2>> {
private final Serializer<S1, T1> keySerializer;
private final Serializer<S2, T2> valSerializer;
private final boolean outputNulls;
private final boolean inputNulls;
public MapSerializer(
Serializer<S1, T1> keySerializer,
Serializer<S2, T2> valSerializer,
boolean outputNulls,
boolean inputNulls
) {
this.keySerializer = Validator.requireNonNull(keySerializer, "key serializer");
this.valSerializer = Validator.requireNonNull(valSerializer, "value serializer");
this.outputNulls = outputNulls;
this.inputNulls = inputNulls;
}
@Override
public Map<T1, T2> serialize(Map<S1, S2> element) {
// cannot work with Collectors.toMap as is doesn't allow null values
final Map<T1, T2> result = new LinkedHashMap<>();
for (final Map.Entry<S1, S2> entry : element.entrySet()) {
if (!outputNulls && isEntryNull(entry))
continue;
var s1key = entry.getKey();
var s2val = entry.getValue();
var t1key = (s1key == null) ? null : keySerializer.serialize(s1key);
var t2val = (s2val == null) ? null : valSerializer.serialize(s2val);
result.put(t1key, t2val);
}
return result;
}
@Override
public Map<S1, S2> deserialize(Map<T1, T2> element) {
// cannot work with Collectors.toMap as is doesn't allow null values
final Map<S1, S2> result = new LinkedHashMap<>();
for (final Map.Entry<T1, T2> entry : element.entrySet()) {
if (!inputNulls && isEntryNull(entry))
continue;
var t1key = entry.getKey();
var t2val = entry.getValue();
var s1key = (t1key == null) ? null : keySerializer.deserialize(t1key);
var s2val = (t2val == null) ? null : valSerializer.deserialize(t2val);
result.put(s1key, s2val);
}
return result;
}
private static boolean isEntryNull(Map.Entry<?, ?> entry) {
return (entry == null) || (entry.getKey() == null) || (entry.getValue() == null);
}
public Serializer<S1, T1> getKeySerializer() {
return keySerializer;
}
public Serializer<S2, T2> getValueSerializer() {
return valSerializer;
}
}
static final class ArraySerializer<T1, T2> implements Serializer<T1[], List<T2>> {
private final Class<?> componentType;
private final Serializer<T1, T2> serializer;
private final boolean outputNulls;
private final boolean inputNulls;
public ArraySerializer(
Class<?> componentType,
Serializer<T1, T2> serializer,
boolean outputNulls,
boolean inputNulls
) {
this.componentType = Validator.requireNonNull(componentType, "component type");
this.serializer = Validator.requireNonNull(serializer, "element serializer");
this.outputNulls = outputNulls;
this.inputNulls = inputNulls;
}
@Override
public List<T2> serialize(T1[] element) {
final Stream<T2> stream = outputNulls
? Arrays.stream(element).map(s -> s == null ? null : serializer.serialize(s))
: Arrays.stream(element).filter(Objects::nonNull).map(serializer::serialize);
return stream.toList();
}
@Override
public T1[] deserialize(List<T2> element) {
final Stream<T1> stream = inputNulls
? element.stream().map(t -> t == null ? null : serializer.deserialize(t))
: element.stream().filter(Objects::nonNull).map(serializer::deserialize);
// The following cast won't fail because we choose the elementSerializer based
// on the componentType.
@SuppressWarnings("unchecked")
IntFunction<T1[]> f = value -> (T1[]) Reflect.newArray(componentType, value);
return stream.toArray(f);
}
public Class<?> getComponentType() {
return componentType;
}
public Serializer<T1, T2> getElementSerializer() {
return serializer;
}
}
static final class PrimitiveBooleanArraySerializer implements Serializer<Object, List<Boolean>> {
@Override
public List<Boolean> serialize(Object element) {
final boolean[] array = (boolean[]) element;
return IntStream.range(0, array.length).mapToObj(i -> array[i]).toList();
}
@Override
public Object deserialize(List<Boolean> element) {
final boolean[] array = new boolean[element.size()];
for (int i = 0; i < element.size(); i++)
array[i] = Validator.requireNonNullArrayElement(element.get(i), "boolean", i);
return array;
}
}
static final class PrimitiveCharacterArraySerializer
implements Serializer<Object, List<String>> {
private static final CharacterSerializer serializer = new CharacterSerializer();
@Override
public List<String> serialize(Object element) {
final char[] array = (char[]) element;
return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i]))
.toList();
}
@Override
public Object deserialize(List<String> element) {
final char[] array = new char[element.size()];
for (int i = 0; i < element.size(); i++) {
String character = Validator.requireNonNullArrayElement(element.get(i), "char", i);
array[i] = serializer.deserialize(character);
}
return array;
}
}
static final class PrimitiveByteArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(byte.class);
@Override
public List<Number> serialize(Object element) {
final byte[] array = (byte[]) element;
return IntStream.range(0, array.length).
mapToObj(i -> serializer.serialize(array[i]))
.toList();
}
@Override
public Object deserialize(List<Number> element) {
final byte[] array = new byte[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "byte", i);
array[i] = (byte) serializer.deserialize(number);
}
return array;
}
}
static final class PrimitiveShortArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(short.class);
@Override
public List<Number> serialize(Object element) {
final short[] array = (short[]) element;
return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i]))
.toList();
}
@Override
public Object deserialize(List<Number> element) {
final short[] array = new short[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "short", i);
array[i] = (short) serializer.deserialize(number);
}
return array;
}
}
static final class PrimitiveIntegerArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(int.class);
@Override
public List<Number> serialize(Object element) {
final int[] array = (int[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList();
}
@Override
public Object deserialize(List<Number> element) {
final int[] array = new int[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "int", i);
array[i] = (int) serializer.deserialize(number);
}
return array;
}
}
static final class PrimitiveLongArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(long.class);
@Override
public List<Number> serialize(Object element) {
final long[] array = (long[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList();
}
@Override
public Object deserialize(List<Number> element) {
final long[] array = new long[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "long", i);
array[i] = (long) serializer.deserialize(number);
}
return array;
}
}
static final class PrimitiveFloatArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(float.class);
@Override
public List<Number> serialize(Object element) {
final float[] array = (float[]) element;
return IntStream.range(0, array.length)
.mapToObj(i -> serializer.serialize(array[i]))
.toList();
}
@Override
public Object deserialize(List<Number> element) {
final float[] array = new float[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "float", i);
array[i] = (float) serializer.deserialize(number);
}
return array;
}
}
static final class PrimitiveDoubleArraySerializer
implements Serializer<Object, List<Number>> {
private static final NumberSerializer serializer = new NumberSerializer(double.class);
@Override
public List<Number> serialize(Object element) {
final double[] array = (double[]) element;
return Arrays.stream(array).mapToObj(serializer::serialize).toList();
}
@Override
public Object deserialize(List<Number> element) {
final double[] array = new double[element.size()];
for (int i = 0; i < element.size(); i++) {
Number number = Validator.requireNonNullArrayElement(element.get(i), "double", i);
array[i] = (double) serializer.deserialize(number);
}
return array;
}
}
}

@ -0,0 +1,36 @@
package de.exlll.configlib;
import java.util.Objects;
final class Validator {
private Validator() {}
static <T> T requireNonNull(T object, String argumentName) {
String msg = "The " + argumentName + " must not be null.";
return Objects.requireNonNull(object, msg);
}
static <T> T requireNonNullArrayElement(T element, String type, int index) {
if (element == null) {
String msg = "The " + type + " element at index " + index + " must not be null.";
throw new ConfigurationException(msg);
}
return element;
}
static <T> Class<T> requireConfiguration(Class<T> cls) {
if (!Reflect.isConfiguration(cls)) {
String msg = "Class '" + cls.getSimpleName() + "' must be a configuration.";
throw new ConfigurationException(msg);
}
return cls;
}
static void requirePrimitiveOrWrapperNumberType(Class<?> cls) {
if (!Reflect.isIntegerType(cls) && !Reflect.isFloatingPointType(cls)) {
String msg = "Class " + cls.getSimpleName() + " is not a byte, short, int, long, " +
"float, double, or a wrapper type of one of the primitive number types.";
throw new IllegalArgumentException(msg);
}
}
}

@ -0,0 +1,62 @@
package de.exlll.configlib;
/**
* An extension of the {@code FileConfigurationProperties} class that allows configuring properties
* that are more specific to YAML files.
*/
public final class YamlConfigurationProperties extends FileConfigurationProperties {
/**
* Constructs a new instance of this class with values taken from the given builder.
*
* @param builder the builder used to initialize the fields of this class
* @throws NullPointerException if the builder or any of its values is null
*/
public YamlConfigurationProperties(Builder<?> builder) {
super(builder);
}
/**
* Constructs a new {@code Builder} with default values.
*
* @return newly constructed {@code Builder}
*/
public static Builder<?> newBuilder() {
return new BuilderImpl();
}
private static final class BuilderImpl extends Builder<BuilderImpl> {
@Override
protected BuilderImpl getThis() {return this;}
@Override
public YamlConfigurationProperties build() {return new YamlConfigurationProperties(this);}
}
/**
* A builder class for constructing {@code YamlConfigurationProperties}.
*
* @param <B> the type of builder
*/
public static abstract class Builder<B extends Builder<B>>
extends FileConfigurationProperties.Builder<B> {
/**
* A constructor that can be overridden by subclasses.
*/
protected Builder() {}
/**
* Builds a {@code ConfigurationProperties} instance.
*
* @return newly constructed {@code ConfigurationProperties}
*/
public abstract YamlConfigurationProperties build();
/**
* Returns this builder.
*
* @return this builder
*/
protected abstract B getThis();
}
}

@ -0,0 +1,286 @@
package de.exlll.configlib;
import org.snakeyaml.engine.v2.api.Dump;
import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.Load;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.common.FlowStyle;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
import org.snakeyaml.engine.v2.nodes.Node;
import org.snakeyaml.engine.v2.nodes.Tag;
import org.snakeyaml.engine.v2.representer.StandardRepresenter;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import static de.exlll.configlib.Validator.requireNonNull;
/**
* A configuration store that saves and loads configurations as YAML text files.
*
* @param <T> the configuration type
*/
public final class YamlConfigurationStore<T> implements FileConfigurationStore<T> {
private static final Dump YAML_DUMPER = newYamlDumper();
private static final Load YAML_LOADER = newYamlLoader();
private final Class<T> configurationType;
private final YamlConfigurationProperties properties;
private final ConfigurationSerializer<T> serializer;
private final CommentNodeExtractor extractor;
/**
* Constructs a new store.
*
* @param configurationType the type of configuration
* @param properties the properties
* @throws NullPointerException if any argument is null
*/
public YamlConfigurationStore(Class<T> configurationType, YamlConfigurationProperties properties) {
this.configurationType = requireNonNull(configurationType, "configuration type");
this.properties = requireNonNull(properties, "properties");
this.serializer = new ConfigurationSerializer<>(configurationType, properties);
this.extractor = new CommentNodeExtractor(properties);
}
@Override
public void save(T configuration, Path configurationFile) {
tryCreateParentDirectories(configurationFile);
var extractedCommentNodes = extractor.extractCommentNodes(configuration);
var yamlFileWriter = new YamlFileWriter(configurationFile, properties);
var dumpedYaml = tryDump(configuration);
yamlFileWriter.writeYaml(dumpedYaml, extractedCommentNodes);
}
private void tryCreateParentDirectories(Path configurationFile) {
Path parent = configurationFile.getParent();
if (!Files.exists(parent) && properties.createParentDirectories()) {
try {
Files.createDirectories(parent);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private String tryDump(T configuration) {
final Map<?, ?> serializedConfiguration = serializer.serialize(configuration);
try {
return YAML_DUMPER.dumpToString(serializedConfiguration);
} catch (YamlEngineException e) {
String msg = "The given configuration could not be converted into YAML. \n" +
"Do all custom serializers produce valid target types?";
throw new ConfigurationException(msg, e);
}
}
@Override
public T load(Path configurationFile) {
try (var reader = Files.newBufferedReader(configurationFile)) {
var yaml = YAML_LOADER.loadFromReader(reader);
var conf = requireConfiguration(yaml, configurationFile);
return serializer.deserialize(conf);
} catch (YamlEngineException e) {
String msg = "The configuration file at %s does not contain valid YAML.";
throw new ConfigurationException(msg.formatted(configurationFile), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Map<?, ?> requireConfiguration(Object yaml, Path configurationFile) {
if (yaml == null) {
String msg = "The configuration file at %s is empty or only contains null.";
throw new ConfigurationException(msg.formatted(configurationFile));
}
if (!(yaml instanceof Map<?, ?>)) {
String msg = "The contents of the YAML file at %s do not represent a configuration. " +
"A valid configuration file contains a YAML map but instead a " +
"'" + yaml.getClass() + "' was found.";
throw new ConfigurationException(msg.formatted(configurationFile));
}
return (Map<?, ?>) yaml;
}
@Override
public T update(Path configurationFile) {
if (Files.exists(configurationFile)) {
T configuration = load(configurationFile);
save(configuration, configurationFile);
return configuration;
}
T configuration = Reflect.newInstance(configurationType);
save(configuration, configurationFile);
return configuration;
}
static Dump newYamlDumper() {
DumpSettings settings = DumpSettings.builder()
.setDefaultFlowStyle(FlowStyle.BLOCK)
.setIndent(2)
.build();
return new Dump(settings, new YamlConfigurationRepresenter(settings));
}
static Load newYamlLoader() {
LoadSettings settings = LoadSettings.builder().build();
return new Load(settings);
}
/**
* A writer that writes YAML to a file.
*/
static final class YamlFileWriter {
private final Path configurationFile;
private final YamlConfigurationProperties properties;
private BufferedWriter writer;
YamlFileWriter(Path configurationFile, YamlConfigurationProperties properties) {
this.configurationFile = requireNonNull(configurationFile, "configuration file");
this.properties = requireNonNull(properties, "configuration properties");
}
public void writeYaml(String yaml, Queue<CommentNode> nodes) {
try (BufferedWriter writer = Files.newBufferedWriter(configurationFile)) {
this.writer = writer;
writeHeader();
writeContent(yaml, nodes);
writeFooter();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
this.writer = null;
}
}
private void writeHeader() throws IOException {
if (properties.getHeader() != null) {
writeAsComment(properties.getHeader());
writer.newLine();
}
}
private void writeFooter() throws IOException {
if (properties.getFooter() != null) {
writer.newLine();
writeAsComment(properties.getFooter());
}
}
private void writeAsComment(String comment) throws IOException {
String[] lines = comment.split("\n");
writeComments(Arrays.asList(lines), 0);
}
private void writeComments(List<String> comments, int indentLevel) throws IOException {
String indent = " ".repeat(indentLevel);
for (String comment : comments) {
if (comment.isEmpty()) {
writer.newLine();
continue;
}
String line = indent + "# " + comment;
writeLine(line);
}
}
private void writeLine(String line) throws IOException {
writer.write(line);
writer.newLine();
}
private void writeContent(String yaml, Queue<CommentNode> nodes) throws IOException {
if (nodes.isEmpty()) {
writer.write(yaml);
} else {
writeCommentedYaml(yaml, nodes);
}
}
private void writeCommentedYaml(String yaml, Queue<CommentNode> nodes)
throws IOException {
/*
* The following algorithm is necessary since no Java YAML library seems
* to properly support comments, at least not the way I want them.
*
* The algorithm writes YAML line by line and keeps track of the current
* context with the help of fieldNames lists which come from the nodes in
* the 'nodes' queue. The 'nodes' queue contains nodes in the order in
* which the fields were read, which happened in DFS manner and with fields
* of a parent class being read before the fields of the child. That order
* ultimately represents the order in which the YAML file is structured.
*/
var node = nodes.poll();
var currentIndentLevel = 0;
for (final String line : yaml.split("\n")) {
if (node == null) {
writeLine(line);
continue;
}
final var fieldNames = node.fieldNames();
final var indent = " ".repeat(currentIndentLevel);
final var lineStart = indent + fieldNames.get(currentIndentLevel) + ":";
if (!line.startsWith(lineStart)) {
writeLine(line);
continue;
}
final var commentIndentLevel = fieldNames.size() - 1;
if (currentIndentLevel++ == commentIndentLevel) {
writeComments(node.comments(), commentIndentLevel);
if ((node = nodes.poll()) != null) {
currentIndentLevel = lengthCommonPrefix(node.fieldNames(), fieldNames);
}
}
writeLine(line);
}
}
static int lengthCommonPrefix(List<String> l1, List<String> l2) {
final int maxLen = Math.min(l1.size(), l2.size());
int result = 0;
for (int i = 0; i < maxLen; i++) {
String s1 = l1.get(i);
String s2 = l2.get(i);
if (s1.equals(s2))
result++;
else return result;
}
return result;
}
}
/**
* A custom representer that prevents aliasing.
*/
static final class YamlConfigurationRepresenter extends StandardRepresenter {
public YamlConfigurationRepresenter(DumpSettings settings) {
super(settings);
}
@Override
protected Node representSequence(Tag tag, Iterable<?> sequence, FlowStyle flowStyle) {
Node node = super.representSequence(tag, sequence, flowStyle);
representedObjects.clear();
return node;
}
@Override
protected Node representMapping(Tag tag, Map<?, ?> mapping, FlowStyle flowStyle) {
Node node = super.representMapping(tag, mapping, flowStyle);
representedObjects.clear();
return node;
}
}
}

@ -0,0 +1,322 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings({"unused", "FieldMayBeFinal"})
class CommentNodeExtractorTest {
private static final ConfigurationProperties PROPERTIES = ConfigurationProperties.newBuilder()
.outputNulls(true)
.build();
private final CommentNodeExtractor EXTRACTOR = new CommentNodeExtractor(PROPERTIES);
@Test
void requiresConfiguration() {
assertThrowsConfigurationException(
() -> EXTRACTOR.extractCommentNodes(new Object()),
"Class 'Object' must be a configuration."
);
}
@Test
void extractSingleComment1() {
@Configuration
class A {
@Comment("Hello")
int i;
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new A());
assertEquals(cn(List.of("Hello"), "i"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractSingleComment2() {
@Configuration
class A {
@Comment({"Hello", "World"})
int i;
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new A());
assertEquals(cn(List.of("Hello", "World"), "i"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractMultipleComments() {
@Configuration
class A {
@Comment("Hello")
int i;
int j;
@Comment("World")
int k;
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new A());
assertEquals(cn(List.of("Hello"), "i"), nodes.poll());
assertEquals(cn(List.of("World"), "k"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractNestedComment1() {
@Configuration
class A {
@Comment("Hello")
int i;
}
@Configuration
class B {
A a = new A();
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new B());
assertEquals(cn(List.of("Hello"), "a", "i"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractNestedComment2() {
@Configuration
class A {
@Comment("Hello")
int i;
int j;
@Comment("World")
int k;
}
@Configuration
class B {
A a = new A();
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new B());
assertEquals(cn(List.of("Hello"), "a", "i"), nodes.poll());
assertEquals(cn(List.of("World"), "a", "k"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractNestedComment3() {
@Configuration
class A {
@Comment("Hello")
int i;
int j;
@Comment("World")
int k;
}
@Configuration
class B {
@Comment("Hello")
A a1 = new A();
@Comment("World")
A a2 = new A();
}
@Configuration
class C {
@Comment("Hello")
B b = new B();
@Comment("World")
A a = new A();
}
@Configuration
class D {
C c = new C();
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new D());
assertEquals(cn(List.of("Hello"), "c", "b"), nodes.poll());
assertEquals(cn(List.of("Hello"), "c", "b", "a1"), nodes.poll());
assertEquals(cn(List.of("Hello"), "c", "b", "a1", "i"), nodes.poll());
assertEquals(cn(List.of("World"), "c", "b", "a1", "k"), nodes.poll());
assertEquals(cn(List.of("World"), "c", "b", "a2"), nodes.poll());
assertEquals(cn(List.of("Hello"), "c", "b", "a2", "i"), nodes.poll());
assertEquals(cn(List.of("World"), "c", "b", "a2", "k"), nodes.poll());
assertEquals(cn(List.of("World"), "c", "a"), nodes.poll());
assertEquals(cn(List.of("Hello"), "c", "a", "i"), nodes.poll());
assertEquals(cn(List.of("World"), "c", "a", "k"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractEmptyClass() {
@Configuration
class A {}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new A());
assertTrue(nodes.isEmpty());
}
@Test
void extractNestedOnlyIfNotNull() {
@Configuration
class A {
@Comment("Hello")
int i;
}
@Configuration
class B {
A a = null;
}
@Configuration
class C {
@Comment("Hello")
A a = null;
}
Queue<CommentNode> nodes1 = EXTRACTOR.extractCommentNodes(new B());
assertTrue(nodes1.isEmpty());
Queue<CommentNode> nodes2 = EXTRACTOR.extractCommentNodes(new C());
assertEquals(cn(List.of("Hello"), "a"), nodes2.poll());
assertTrue(nodes2.isEmpty());
}
@Test
void extractIgnoresCommentIfFieldNullAndOutputNull1() {
@Configuration
class A {
@Comment("Hello")
int i;
}
@Configuration
class B {
A a = null;
}
@Configuration
class C {
@Comment("Hello")
A a = null;
}
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.outputNulls(false)
.build();
CommentNodeExtractor extractor = new CommentNodeExtractor(properties);
Queue<CommentNode> nodes1 = extractor.extractCommentNodes(new B());
assertTrue(nodes1.isEmpty());
Queue<CommentNode> nodes2 = extractor.extractCommentNodes(new C());
assertTrue(nodes2.isEmpty());
}
@Test
void extractIgnoresCommentIfFieldNullAndOutputNull2() {
@Configuration
class A {
@Comment("Hello")
String s1 = null;
@Comment("World")
String s2 = "";
}
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.outputNulls(false)
.build();
CommentNodeExtractor extractor = new CommentNodeExtractor(properties);
Queue<CommentNode> nodes = extractor.extractCommentNodes(new A());
assertEquals(cn(List.of("World"), "s2"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractAppliesFormatterAndFilter() {
@Configuration
class A {
@Comment("Hello")
int i;
int j;
@Comment("World")
int k;
}
@Configuration
class B {
@Comment("Hello")
A a1 = new A();
@Comment("World")
A a2 = new A();
}
@Configuration
class C {
@Comment("Hello")
B b = new B();
@Comment("World")
A a = new A();
}
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.setFieldFilter(field -> {
String name = field.getName();
return !name.equals("a1") && !name.equals("a");
})
.setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
.build();
CommentNodeExtractor extractor = new CommentNodeExtractor(properties);
Queue<CommentNode> nodes = extractor.extractCommentNodes(new C());
assertEquals(cn(List.of("Hello"), "B"), nodes.poll());
assertEquals(cn(List.of("World"), "B", "A2"), nodes.poll());
assertEquals(cn(List.of("Hello"), "B", "A2", "I"), nodes.poll());
assertEquals(cn(List.of("World"), "B", "A2", "K"), nodes.poll());
assertTrue(nodes.isEmpty());
}
@Test
void extractIgnoresCommentsForConfigurationsInCollections() {
@Configuration
class A {
@Comment("Yes")
int i;
}
@Configuration
class B {
Set<A> setA;
List<A> listA;
Map<Integer, A> mapIntegerA;
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new B());
assertTrue(nodes.isEmpty());
}
@Test
void extractCommentsFromParentClasses() {
@Configuration
class A1 {
@Comment({"int", "i"})
int i;
}
@Configuration
class A2 extends A1 {
@Comment({"int", "j"})
int j;
}
@Configuration
class B1 {
@Comment({"A2", "a2"})
A2 a2 = new A2();
}
@Configuration
class B2 extends B1 {
@Comment({"A1", "a1"})
A1 a1 = new A1();
}
Queue<CommentNode> nodes = EXTRACTOR.extractCommentNodes(new B2());
assertEquals(cn(List.of("A2", "a2"), "a2"), nodes.poll());
assertEquals(cn(List.of("int", "i"), "a2", "i"), nodes.poll());
assertEquals(cn(List.of("int", "j"), "a2", "j"), nodes.poll());
assertEquals(cn(List.of("A1", "a1"), "a1"), nodes.poll());
assertEquals(cn(List.of("int", "i"), "a1", "i"), nodes.poll());
assertTrue(nodes.isEmpty());
}
private static CommentNode cn(List<String> comments, String... fieldNames) {
return new CommentNode(comments, List.of(fieldNames));
}
}

@ -0,0 +1,94 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import java.util.Locale;
import java.util.Map;
import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ConfigurationPropertiesTest {
@Test
void builderDefaultValues() {
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
assertThat(properties.serializeSetsAsLists(), is(true));
assertThat(properties.outputNulls(), is(false));
assertThat(properties.inputNulls(), is(false));
assertThat(properties.getSerializers().entrySet(), empty());
assertThat(properties.getFieldFormatter(), is(FieldFormatters.IDENTITY));
assertThat(properties.getFieldFilter(), is(FieldFilters.DEFAULT));
}
@Test
void builderCopiesValues() {
FieldFormatter formatter = field -> field.getName().toLowerCase(Locale.ROOT);
FieldFilter filter = field -> field.getName().startsWith("f");
TestUtils.PointSerializer serializer = new TestUtils.PointSerializer();
ConfigurationProperties properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, serializer)
.setFieldFormatter(formatter)
.setFieldFilter(filter)
.outputNulls(true)
.inputNulls(true)
.serializeSetsAsLists(false)
.build();
assertThat(properties.getSerializers(), is(Map.of(Point.class, serializer)));
assertThat(properties.outputNulls(), is(true));
assertThat(properties.inputNulls(), is(true));
assertThat(properties.serializeSetsAsLists(), is(false));
assertThat(properties.getFieldFormatter(), sameInstance(formatter));
assertThat(properties.getFieldFilter(), sameInstance(filter));
}
@Test
void builderSerializersUnmodifiable() {
ConfigurationProperties properties = ConfigurationProperties.newBuilder().build();
Map<Class<?>, Serializer<?, ?>> map = properties.getSerializers();
assertThrows(
UnsupportedOperationException.class,
() -> map.put(Point.class, new TestUtils.PointSerializer())
);
}
public static final class BuilderTest {
private static final ConfigurationProperties.Builder<?> builder = ConfigurationProperties.newBuilder();
@Test
void setFieldFilterRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.setFieldFilter(null),
"field filter"
);
}
@Test
void setFieldFormatterRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.setFieldFormatter(null),
"field formatter"
);
}
@Test
void addSerializerRequiresNonNull() {
assertThrowsNullPointerException(
() -> builder.addSerializer(null, new Serializers.StringSerializer()),
"serialized type"
);
assertThrowsNullPointerException(
() -> builder.addSerializer(String.class, null),
"serializer"
);
}
}
}

@ -0,0 +1,317 @@
package de.exlll.configlib;
import de.exlll.configlib.ConfigurationProperties.Builder;
import de.exlll.configlib.Serializers.*;
import de.exlll.configlib.configurations.*;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import static de.exlll.configlib.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;
@SuppressWarnings("FieldMayBeFinal")
class ConfigurationSerializerTest {
private static <T> ConfigurationSerializer<T> newSerializer(Class<T> cls) {
return newSerializer(cls, builder -> builder);
}
private static <T> ConfigurationSerializer<T> newSerializer(
Class<T> cls,
Function<Builder<?>, Builder<?>> propertiesConfigurer
) {
var builder = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER);
builder.addSerializer(Point.class, TestUtils.POINT_SERIALIZER);
builder = propertiesConfigurer.apply(builder);
return new ConfigurationSerializer<>(cls, builder.build());
}
@Test
void buildSerializerMapFiltersFields() {
Map<String, Serializer<?, ?>> serializers = newSerializer(ExampleConfigurationA2.class)
.buildSerializerMap();
assertThat(serializers.get("a1_staticFinalInt"), nullValue());
assertThat(serializers.get("a1_staticInt"), nullValue());
assertThat(serializers.get("a1_finalInt"), nullValue());
assertThat(serializers.get("a1_transientInt"), nullValue());
assertThat(serializers.get("a1_ignoredInt"), nullValue());
assertThat(serializers.get("a1_ignoredString"), nullValue());
assertThat(serializers.get("a1_ignoredListString"), nullValue());
assertThat(serializers.get("a2_staticFinalInt"), nullValue());
assertThat(serializers.get("a2_staticInt"), nullValue());
assertThat(serializers.get("a2_finalInt"), nullValue());
assertThat(serializers.get("a2_transientInt"), nullValue());
assertThat(serializers.get("a2_ignoredInt"), nullValue());
assertThat(serializers.get("a2_ignoredString"), nullValue());
assertThat(serializers.get("a2_ignoredListString"), nullValue());
}
@Test
void buildSerializerMapIgnoresFormatter() {
Map<String, Serializer<?, ?>> serializers = newSerializer(
ExampleConfigurationA2.class,
props -> props.setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
).buildSerializerMap();
assertThat(serializers.get("A2_PRIM_BOOL"), nullValue());
assertThat(serializers.get("a2_primBool"), instanceOf(BooleanSerializer.class));
}
@Test
void buildSerializerMap() {
Map<String, Serializer<?, ?>> serializers = newSerializer(ExampleConfigurationA2.class)
.buildSerializerMap();
assertThat(serializers.get("a2_primBool"), instanceOf(BooleanSerializer.class));
assertThat(serializers.get("a2_refChar"), instanceOf(CharacterSerializer.class));
assertThat(serializers.get("a2_string"), instanceOf(StringSerializer.class));
assertThat(serializers.get("a2_Enm"), instanceOf(EnumSerializer.class));
ConfigurationSerializer<?> serializerB1 =
(ConfigurationSerializer<?>) serializers.get("a2_b1");
ConfigurationSerializer<?> serializerB2 =
(ConfigurationSerializer<?>) serializers.get("a2_b2");
assertThat(serializerB1.getConfigurationType(), equalTo(ExampleConfigurationB1.class));
assertThat(serializerB2.getConfigurationType(), equalTo(ExampleConfigurationB2.class));
ListSerializer<?, ?> serializerList =
(ListSerializer<?, ?>) serializers.get("a2_listByte");
ArraySerializer<?, ?> serializerArray =
(ArraySerializer<?, ?>) serializers.get("a2_arrayString");
SetAsListSerializer<?, ?> serializerSet =
(SetAsListSerializer<?, ?>) serializers.get("a2_setBigInteger");
MapSerializer<?, ?, ?, ?> serializerMap =
(MapSerializer<?, ?, ?, ?>) serializers.get("a2_mapLocalTimeLocalTime");
assertThat(
serializers.get("a2_arrayPrimDouble"),
instanceOf(PrimitiveDoubleArraySerializer.class)
);
assertThat(serializerList.getElementSerializer(), instanceOf(NumberSerializer.class));
assertThat(serializerArray.getElementSerializer(), instanceOf(StringSerializer.class));
assertThat(serializerSet.getElementSerializer(), instanceOf(BigIntegerSerializer.class));
assertThat(serializerMap.getKeySerializer(), instanceOf(LocalTimeSerializer.class));
assertThat(serializerMap.getValueSerializer(), instanceOf(LocalTimeSerializer.class));
assertThat(serializers.get("a2_point"), sameInstance(TestUtils.POINT_SERIALIZER));
}
@Test
void serializeAppliesFormatter() {
@Configuration
class A {
int value1 = 1;
int someValue2 = 2;
}
ConfigurationSerializer<A> serializer = newSerializer(
A.class,
builder -> builder.setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
);
Map<?, ?> map = serializer.serialize(new A());
assertThat(map.remove("VALUE1"), is(1L));
assertThat(map.remove("SOME_VALUE2"), is(2L));
assertTrue(map.isEmpty());
}
@Configuration
private static final class B1 {
int value1 = 1;
int someValue2 = 2;
}
@Test
void deserializeAppliesFormatter() {
ConfigurationSerializer<B1> serializer = newSerializer(
B1.class,
builder -> builder.setFieldFormatter(FieldFormatters.UPPER_UNDERSCORE)
);
Map<String, ?> map = Map.of(
"value1", 3,
"someValue2", 4,
"VALUE1", 5,
"SOME_VALUE2", 6
);
B1 a = serializer.deserialize(map);
assertThat(a.value1, is(5));
assertThat(a.someValue2, is(6));
}
@Configuration
private static final class B2 {
boolean f1;
char f2;
byte f3;
short f4;
int f5;
long f6;
float f7;
double f8;
}
@Test
void deserializeNullForPrimitiveFields() {
ConfigurationSerializer<B2> serializer = newSerializer(
B2.class,
builder -> builder.inputNulls(true)
);
for (int i = 1; i <= 8; i++) {
String fieldName = "f" + i;
Map<String, Object> map = asMap(fieldName, null);
assertThrowsConfigurationException(
() -> serializer.deserialize(map),
"Cannot set " + getField(B2.class, fieldName) + " to null value.\n" +
"Primitive types cannot be assigned null."
);
}
}
@Configuration
private static final class B3 {
String s = "";
List<List<String>> l = List.of();
}
@Test
void deserializeInvalidType() {
ConfigurationSerializer<B3> serializer = newSerializer(B3.class);
assertThrowsConfigurationException(
() -> serializer.deserialize(Map.of("s", (byte) 3)),
"Deserialization of value '3' with type class java.lang.Byte for field " +
"java.lang.String de.exlll.configlib.ConfigurationSerializerTest$B3.s " +
"failed.\nThe type of the object to be deserialized does not match the type " +
"the deserializer expects."
);
assertThrowsConfigurationException(
() -> serializer.deserialize(Map.of("l", List.of(List.of(3)))),
"Deserialization of value '[[3]]' with type class " +
"java.util.ImmutableCollections$List12 for field java.util.List " +
"de.exlll.configlib.ConfigurationSerializerTest$B3.l failed.\n" +
"The type of the object to be deserialized does not match the type the " +
"deserializer expects."
);
}
@Configuration
private static final class B4 {
private static final ExampleConfigurationB1 B4_NULL_B1 =
ExampleInitializer.newExampleConfigurationB1_1();
private static final List<String> B4_NULL_LIST = List.of();
private static final Double[] B4_NULL_ARRAY = new Double[0];
private static final Set<ExampleConfigurationB2> B4_NULL_SET = Set.of();
private static final Map<LocalDate, BigDecimal> B4_NULL_MAP = Map.of();
private static final Point B4_NULL_POINT = new Point(0, 0);
Integer nullInteger = 1;
String nullString = "";
ExampleEnum nullEnm = ExampleEnum.A;
ExampleConfigurationB1 nullB1 = B4_NULL_B1;
List<String> nullList = B4_NULL_LIST;
Double[] nullArray = B4_NULL_ARRAY;
Set<ExampleConfigurationB2> nullSet = B4_NULL_SET;
Map<LocalDate, BigDecimal> nullMap = B4_NULL_MAP;
Point nullPoint = B4_NULL_POINT;
}
@Test
void deserializeWithoutNullDoesNotOverrideInitializedFields() {
final Map<String, ?> map = entriesAsMap(
entry("nullInteger", null),
entry("nullString", null),
entry("nullEnm", null),
entry("nullB1", null),
entry("nullList", null),
entry("nullArray", null),
entry("nullSet", null),
entry("nullMap", null),
entry("nullPoint", null)
);
ConfigurationSerializer<B4> serializer = newSerializer(B4.class);
B4 config = serializer.deserialize(map);
assertEquals(1, config.nullInteger);
assertEquals("", config.nullString);
assertEquals(ExampleEnum.A, config.nullEnm);
assertSame(B4.B4_NULL_B1, config.nullB1);
assertSame(B4.B4_NULL_LIST, config.nullList);
assertSame(B4.B4_NULL_ARRAY, config.nullArray);
assertSame(B4.B4_NULL_SET, config.nullSet);
assertSame(B4.B4_NULL_MAP, config.nullMap);
assertSame(B4.B4_NULL_POINT, config.nullPoint);
}
@Configuration
private static final class B5 {
@Ignore
private int ignored = 1;
}
@Test
void ctorRequiresConfigurationWithFields() {
assertThrowsConfigurationException(
() -> newSerializer(B5.class),
"Configuration class 'B5' does not contain any (de-)serializable fields."
);
}
private static final class B6 {
@Ignore
private int ignored = 1;
}
@Test
void ctorRequiresConfiguration() {
assertThrowsConfigurationException(
() -> newSerializer(B6.class),
"Class 'B6' must be a configuration."
);
}
@Test
void buildSerializerMapPreventsRecursiveDefinitions() {
assertThrowsConfigurationException(
() -> newSerializer(R1.class),
"Recursive type definitions are not supported."
);
}
@Configuration
static final class R1 {
R2 r2;
}
@Configuration
static final class R2 {
R1 r1;
}
@Test
void serializeTypeWithAbstractParent() {
ConfigurationSerializer<B8> serializer = newSerializer(B8.class);
Map<?, ?> serialize = serializer.serialize(new B8());
B8 deserialize = serializer.deserialize(serialize);
assertEquals(1, deserialize.i);
assertEquals(2, deserialize.j);
}
@Configuration
static abstract class B7 {
int i = 1;
}
static final class B8 extends B7 {
int j = 2;
}
}

@ -0,0 +1,147 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ConfigurationsTest {
private static final FieldFilter includeI = field -> field.getName().equals("i");
private final FileSystem fs = Jimfs.newFileSystem();
private final Path yamlFile = fs.getPath("/tmp/config.yml");
@BeforeEach
void setUp() throws IOException {
Files.createDirectories(yamlFile.getParent());
}
@AfterEach
void tearDown() throws IOException {
fs.close();
}
@Configuration
private static final class Config {
int i = 10;
int j = 11;
}
@Test
void saveYamlConfiguration1() {
Config configuration = new Config();
Configurations.saveYamlConfiguration(yamlFile, Config.class, configuration);
assertEquals("i: 10\nj: 11", TestUtils.readFile(yamlFile));
configuration.i = 20;
Configurations.saveYamlConfiguration(yamlFile, Config.class, configuration);
assertEquals("i: 20\nj: 11", TestUtils.readFile(yamlFile));
}
@Test
void saveYamlConfiguration2() {
Config configuration = new Config();
Configurations.saveYamlConfiguration(
yamlFile, Config.class, configuration,
builder -> builder.setFieldFilter(includeI)
);
assertEquals("i: 10", TestUtils.readFile(yamlFile));
}
@Test
void saveYamlConfiguration3() {
Config configuration = new Config();
Configurations.saveYamlConfiguration(
yamlFile, Config.class, configuration,
YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build()
);
assertEquals("i: 10", TestUtils.readFile(yamlFile));
}
@Test
void loadYamlConfiguration1() {
writeString("i: 20\nk: 30");
Config config = Configurations.loadYamlConfiguration(yamlFile, Config.class);
assertConfigEquals(config, 20, 11);
writeString("i: 20\nj: 30");
config = Configurations.loadYamlConfiguration(yamlFile, Config.class);
assertConfigEquals(config, 20, 30);
}
@Test
void loadYamlConfiguration2() {
writeString("i: 20\nj: 30");
Config config = Configurations.loadYamlConfiguration(
yamlFile, Config.class,
builder -> builder.setFieldFilter(includeI)
);
assertConfigEquals(config, 20, 11);
}
@Test
void loadYamlConfiguration3() {
writeString("i: 20\nj: 30");
Config config = Configurations.loadYamlConfiguration(
yamlFile, Config.class,
YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build()
);
assertConfigEquals(config, 20, 11);
}
@Test
void updateYamlConfiguration1() {
Config config = Configurations.updateYamlConfiguration(yamlFile, Config.class);
assertConfigEquals(config, 10, 11);
assertEquals("i: 10\nj: 11", TestUtils.readFile(yamlFile));
writeString("i: 20\nk: 30");
config = Configurations.updateYamlConfiguration(yamlFile, Config.class);
assertConfigEquals(config, 20, 11);
assertEquals("i: 20\nj: 11", TestUtils.readFile(yamlFile));
}
@Test
void updateYamlConfiguration2() {
Config config = Configurations.updateYamlConfiguration(
yamlFile, Config.class,
builder -> builder.setFieldFilter(includeI)
);
assertConfigEquals(config, 10, 11);
assertEquals("i: 10", TestUtils.readFile(yamlFile));
}
@Test
void updateYamlConfiguration3() {
Config config = Configurations.updateYamlConfiguration(
yamlFile, Config.class,
YamlConfigurationProperties.newBuilder().setFieldFilter(includeI).build()
);
assertConfigEquals(config, 10, 11);
assertEquals("i: 10", TestUtils.readFile(yamlFile));
}
private static void assertConfigEquals(Config config, int i, int j) {
assertEquals(i, config.i);
assertEquals(j, config.j);
}
private void writeString(String string) {
try {
Files.writeString(yamlFile, string);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,673 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.configurations.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static de.exlll.configlib.configurations.ExampleConfigurationsSerialized.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ExampleConfigurationTests {
private static final ConfigurationProperties PROPERTIES_ALLOW_NULL = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER)
.outputNulls(true)
.inputNulls(true)
.build();
private static final ConfigurationProperties PROPERTIES_DENY_NULL = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER)
.outputNulls(false)
.inputNulls(false)
.build();
private final FileSystem fs = Jimfs.newFileSystem();
private final Path yamlFile = fs.getPath("/tmp/config.yml");
@BeforeEach
void setUp() throws IOException {
Files.createDirectories(yamlFile.getParent());
}
@AfterEach
void tearDown() throws IOException {
fs.close();
}
@Test
void serializeExampleConfigurationB1() {
ConfigurationSerializer<ExampleConfigurationB1> serializer =
new ConfigurationSerializer<>(ExampleConfigurationB1.class, PROPERTIES_ALLOW_NULL);
ExampleConfigurationB1 config1 = ExampleInitializer.newExampleConfigurationB1_1();
Map<?, ?> serialized1 = serializer.serialize(config1);
assertEquals(EXAMPLE_CONFIGURATION_B1_1, serialized1);
ExampleConfigurationB1 config2 = ExampleInitializer.newExampleConfigurationB1_2();
Map<?, ?> serialized2 = serializer.serialize(config2);
assertEquals(EXAMPLE_CONFIGURATION_B1_2, serialized2);
}
@Test
void deserializeExampleConfigurationB1() {
ConfigurationSerializer<ExampleConfigurationB1> serializer =
new ConfigurationSerializer<>(ExampleConfigurationB1.class, PROPERTIES_ALLOW_NULL);
assertExampleConfigurationsB1Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_B1_1),
ExampleInitializer.newExampleConfigurationB1_1()
);
assertExampleConfigurationsB1Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_B1_2),
ExampleInitializer.newExampleConfigurationB1_2()
);
}
@Test
void serializeExampleConfigurationB2() {
ConfigurationSerializer<ExampleConfigurationB2> serializer =
new ConfigurationSerializer<>(ExampleConfigurationB2.class, PROPERTIES_ALLOW_NULL);
ExampleConfigurationB2 config1 = ExampleInitializer.newExampleConfigurationB2_1();
Map<?, ?> serialized1 = serializer.serialize(config1);
assertEquals(EXAMPLE_CONFIGURATION_B2_1, serialized1);
ExampleConfigurationB2 config2 = ExampleInitializer.newExampleConfigurationB2_2();
Map<?, ?> serialized2 = serializer.serialize(config2);
assertEquals(EXAMPLE_CONFIGURATION_B2_2, serialized2);
}
@Test
void deserializeExampleConfigurationB2() {
ConfigurationSerializer<ExampleConfigurationB2> serializer =
new ConfigurationSerializer<>(ExampleConfigurationB2.class, PROPERTIES_ALLOW_NULL);
assertExampleConfigurationsB2Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_B2_1),
ExampleInitializer.newExampleConfigurationB2_1()
);
assertExampleConfigurationsB2Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_B2_2),
ExampleInitializer.newExampleConfigurationB2_2()
);
}
@Test
void serializeExampleConfigurationA1() {
ConfigurationSerializer<ExampleConfigurationA1> serializer =
new ConfigurationSerializer<>(ExampleConfigurationA1.class, PROPERTIES_ALLOW_NULL);
ExampleConfigurationA1 config = ExampleInitializer.newExampleConfigurationA2();
Map<?, ?> serialized = serializer.serialize(config);
assertEquals(EXAMPLE_CONFIGURATION_A1, serialized);
}
@Test
void deserializeExampleConfigurationA1() {
ConfigurationSerializer<ExampleConfigurationA1> serializer =
new ConfigurationSerializer<>(ExampleConfigurationA1.class, PROPERTIES_ALLOW_NULL);
assertExampleConfigurationsA1Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_A1),
ExampleInitializer.newExampleConfigurationA2()
);
assertExampleConfigurationsA1Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_A2),
ExampleInitializer.newExampleConfigurationA2()
);
}
@Test
void serializeExampleConfigurationA2() {
ConfigurationSerializer<ExampleConfigurationA2> serializer =
new ConfigurationSerializer<>(ExampleConfigurationA2.class, PROPERTIES_ALLOW_NULL);
ExampleConfigurationA2 config = ExampleInitializer.newExampleConfigurationA2();
Map<?, ?> serialized = serializer.serialize(config);
assertEquals(EXAMPLE_CONFIGURATION_A2, serialized);
}
@Test
void deserializeExampleConfigurationA2() {
ConfigurationSerializer<ExampleConfigurationA2> serializer =
new ConfigurationSerializer<>(ExampleConfigurationA2.class, PROPERTIES_ALLOW_NULL);
assertExampleConfigurationsA2Equal(
serializer.deserialize(EXAMPLE_CONFIGURATION_A2),
ExampleInitializer.newExampleConfigurationA2()
);
}
@Test
void serializeExampleConfigurationsNullsWithNullCollectionElements() {
ConfigurationSerializer<ExampleConfigurationNulls> serializer =
new ConfigurationSerializer<>(ExampleConfigurationNulls.class, PROPERTIES_ALLOW_NULL);
ExampleConfigurationNulls config1 = ExampleInitializer
.newExampleConfigurationNullsWithNullCollectionElements1();
Map<?, ?> serialized1 = serializer.serialize(config1);
assertEquals(EXAMPLE_CONFIGURATION_NULLS_WITH_1, serialized1);
ExampleConfigurationNulls config2 = ExampleInitializer
.newExampleConfigurationNullsWithNullCollectionElements2();
Map<?, ?> serialized2 = serializer.serialize(config2);
assertEquals(EXAMPLE_CONFIGURATION_NULLS_WITH_2, serialized2);
}
@Test
void deserializeExampleConfigurationNullsWithNullCollectionElements() {
ConfigurationSerializer<ExampleConfigurationNulls> serializer =
new ConfigurationSerializer<>(ExampleConfigurationNulls.class, PROPERTIES_ALLOW_NULL);
assertExampleConfigurationsNullsEqual(
serializer.deserialize(EXAMPLE_CONFIGURATION_NULLS_WITH_1),
ExampleInitializer.newExampleConfigurationNullsWithNullCollectionElements1()
);
assertExampleConfigurationsNullsEqual(
serializer.deserialize(EXAMPLE_CONFIGURATION_NULLS_WITH_2),
ExampleInitializer.newExampleConfigurationNullsWithNullCollectionElements2()
);
}
@Test
void serializeExampleConfigurationsNullsWithoutNullCollectionElements() {
ConfigurationSerializer<ExampleConfigurationNulls> serializer =
new ConfigurationSerializer<>(ExampleConfigurationNulls.class, PROPERTIES_DENY_NULL);
ExampleConfigurationNulls config1 = ExampleInitializer
.newExampleConfigurationNullsWithoutNullCollectionElements1();
Map<?, ?> serialized1 = serializer.serialize(config1);
assertEquals(EXAMPLE_CONFIGURATION_NULLS_WITHOUT_1, serialized1);
ExampleConfigurationNulls config2 = ExampleInitializer
.newExampleConfigurationNullsWithoutNullCollectionElements2();
Map<?, ?> serialized2 = serializer.serialize(config2);
assertEquals(EXAMPLE_CONFIGURATION_NULLS_WITHOUT_2, serialized2);
}
@Test
void deserializeExampleConfigurationNullsWithoutNullCollectionElements() {
ConfigurationSerializer<ExampleConfigurationNulls> serializer =
new ConfigurationSerializer<>(ExampleConfigurationNulls.class, PROPERTIES_DENY_NULL);
assertExampleConfigurationsNullsEqual(
ExampleInitializer.newExampleConfigurationNullsWithoutNullCollectionElements1(),
serializer.deserialize(EXAMPLE_CONFIGURATION_NULLS_WITHOUT_1)
);
assertExampleConfigurationsNullsEqual(
ExampleInitializer.newExampleConfigurationNullsWithoutNullCollectionElements2(),
serializer.deserialize(EXAMPLE_CONFIGURATION_NULLS_WITHOUT_2)
);
}
private static void assertExampleConfigurationsNullsEqual(
ExampleConfigurationNulls expected,
ExampleConfigurationNulls actual
) {
assertEquals(expected.getNullInteger(), actual.getNullInteger());
assertEquals(expected.getNullString(), actual.getNullString());
assertEquals(expected.getNullEnm(), actual.getNullEnm());
assertEquals(expected.getNullB1(), actual.getNullB1());
assertEquals(expected.getNullList(), actual.getNullList());
assertEquals(expected.getNullArray(), actual.getNullArray());
assertEquals(expected.getNullSet(), actual.getNullSet());
assertEquals(expected.getNullMap(), actual.getNullMap());
assertEquals(expected.getNullPoint(), actual.getNullPoint());
assertEquals(expected.getListNullString(), actual.getListNullString());
assertArrayEquals(expected.getArrayNullDouble(), actual.getArrayNullDouble());
assertEquals(expected.getSetNullInteger(), actual.getSetNullInteger());
assertEquals(expected.getMapNullEnmKey(), actual.getMapNullEnmKey());
assertEquals(expected.getMapNullBigIntegerValue(), actual.getMapNullBigIntegerValue());
assertEquals(expected, actual);
}
private static void assertExampleConfigurationsA1Equal(
ExampleConfigurationA1 a1_1,
ExampleConfigurationA1 a1_2
) {
assertThat(ExampleConfigurationA1.getA1_staticFinalInt(), is(1));
assertThat(ExampleConfigurationA1.getA1_staticInt(), is(2));
assertThat(a1_1.getA1_finalInt(), is(a1_2.getA1_finalInt()));
assertThat(a1_1.getA1_transientInt(), is(a1_2.getA1_transientInt()));
assertThat(a1_1.getA1_ignoredInt(), is(a1_2.getA1_ignoredInt()));
assertThat(a1_1.getA1_ignoredString(), is(a1_2.getA1_ignoredString()));
assertThat(a1_1.getA1_ignoredListString(), is(a1_2.getA1_ignoredListString()));
assertThat(a1_1.isA1_primBool(), is(a1_2.isA1_primBool()));
assertThat(a1_1.getA1_primChar(), is(a1_2.getA1_primChar()));
assertThat(a1_1.getA1_primByte(), is(a1_2.getA1_primByte()));
assertThat(a1_1.getA1_primShort(), is(a1_2.getA1_primShort()));
assertThat(a1_1.getA1_primInt(), is(a1_2.getA1_primInt()));
assertThat(a1_1.getA1_primLong(), is(a1_2.getA1_primLong()));
assertThat(a1_1.getA1_primFloat(), is(a1_2.getA1_primFloat()));
assertThat(a1_1.getA1_primDouble(), is(a1_2.getA1_primDouble()));
assertThat(a1_1.getA1_refBool(), is(a1_2.getA1_refBool()));
assertThat(a1_1.getA1_refChar(), is(a1_2.getA1_refChar()));
assertThat(a1_1.getA1_refByte(), is(a1_2.getA1_refByte()));
assertThat(a1_1.getA1_refShort(), is(a1_2.getA1_refShort()));
assertThat(a1_1.getA1_refInt(), is(a1_2.getA1_refInt()));
assertThat(a1_1.getA1_refLong(), is(a1_2.getA1_refLong()));
assertThat(a1_1.getA1_refFloat(), is(a1_2.getA1_refFloat()));
assertThat(a1_1.getA1_refDouble(), is(a1_2.getA1_refDouble()));
assertThat(a1_1.getA1_string(), is(a1_2.getA1_string()));
assertThat(a1_1.getA1_bigInteger(), is(a1_2.getA1_bigInteger()));
assertThat(a1_1.getA1_bigDecimal(), is(a1_2.getA1_bigDecimal()));
assertThat(a1_1.getA1_localDate(), is(a1_2.getA1_localDate()));
assertThat(a1_1.getA1_localTime(), is(a1_2.getA1_localTime()));
assertThat(a1_1.getA1_localDateTime(), is(a1_2.getA1_localDateTime()));
assertThat(a1_1.getA1_Enm(), is(a1_2.getA1_Enm()));
assertThat(a1_1.getA1_b1(), is(a1_2.getA1_b1()));
assertThat(a1_1.getA1_b2(), is(a1_2.getA1_b2()));
assertThat(a1_1.getA1_listBoolean(), is(a1_2.getA1_listBoolean()));
assertThat(a1_1.getA1_listChar(), is(a1_2.getA1_listChar()));
assertThat(a1_1.getA1_listByte(), is(a1_2.getA1_listByte()));
assertThat(a1_1.getA1_listShort(), is(a1_2.getA1_listShort()));
assertThat(a1_1.getA1_listInteger(), is(a1_2.getA1_listInteger()));
assertThat(a1_1.getA1_listLong(), is(a1_2.getA1_listLong()));
assertThat(a1_1.getA1_listFloat(), is(a1_2.getA1_listFloat()));
assertThat(a1_1.getA1_listDouble(), is(a1_2.getA1_listDouble()));
assertThat(a1_1.getA1_listString(), is(a1_2.getA1_listString()));
assertThat(a1_1.getA1_listBigInteger(), is(a1_2.getA1_listBigInteger()));
assertThat(a1_1.getA1_listBigDecimal(), is(a1_2.getA1_listBigDecimal()));
assertThat(a1_1.getA1_listLocalDate(), is(a1_2.getA1_listLocalDate()));
assertThat(a1_1.getA1_listLocalTime(), is(a1_2.getA1_listLocalTime()));
assertThat(a1_1.getA1_listLocalDateTime(), is(a1_2.getA1_listLocalDateTime()));
assertThat(a1_1.getA1_listEnm(), is(a1_2.getA1_listEnm()));
assertThat(a1_1.getA1_listB1(), is(a1_2.getA1_listB1()));
assertThat(a1_1.getA1_listB2(), is(a1_2.getA1_listB2()));
assertThat(a1_1.getA1_arrayPrimBoolean(), is(a1_2.getA1_arrayPrimBoolean()));
assertThat(a1_1.getA1_arrayPrimChar(), is(a1_2.getA1_arrayPrimChar()));
assertThat(a1_1.getA1_arrayPrimByte(), is(a1_2.getA1_arrayPrimByte()));
assertThat(a1_1.getA1_arrayPrimShort(), is(a1_2.getA1_arrayPrimShort()));
assertThat(a1_1.getA1_arrayPrimInteger(), is(a1_2.getA1_arrayPrimInteger()));
assertThat(a1_1.getA1_arrayPrimLong(), is(a1_2.getA1_arrayPrimLong()));
assertThat(a1_1.getA1_arrayPrimFloat(), is(a1_2.getA1_arrayPrimFloat()));
assertThat(a1_1.getA1_arrayPrimDouble(), is(a1_2.getA1_arrayPrimDouble()));
assertThat(a1_1.getA1_arrayBoolean(), is(a1_2.getA1_arrayBoolean()));
assertThat(a1_1.getA1_arrayChar(), is(a1_2.getA1_arrayChar()));
assertThat(a1_1.getA1_arrayByte(), is(a1_2.getA1_arrayByte()));
assertThat(a1_1.getA1_arrayShort(), is(a1_2.getA1_arrayShort()));
assertThat(a1_1.getA1_arrayInteger(), is(a1_2.getA1_arrayInteger()));
assertThat(a1_1.getA1_arrayLong(), is(a1_2.getA1_arrayLong()));
assertThat(a1_1.getA1_arrayFloat(), is(a1_2.getA1_arrayFloat()));
assertThat(a1_1.getA1_arrayDouble(), is(a1_2.getA1_arrayDouble()));
assertThat(a1_1.getA1_arrayString(), is(a1_2.getA1_arrayString()));
assertThat(a1_1.getA1_arrayBigInteger(), is(a1_2.getA1_arrayBigInteger()));
assertThat(a1_1.getA1_arrayBigDecimal(), is(a1_2.getA1_arrayBigDecimal()));
assertThat(a1_1.getA1_arrayLocalDate(), is(a1_2.getA1_arrayLocalDate()));
assertThat(a1_1.getA1_arrayLocalTime(), is(a1_2.getA1_arrayLocalTime()));
assertThat(a1_1.getA1_arrayLocalDateTime(), is(a1_2.getA1_arrayLocalDateTime()));
assertThat(a1_1.getA1_arrayEnm(), is(a1_2.getA1_arrayEnm()));
assertThat(a1_1.getA1_arrayB1(), is(a1_2.getA1_arrayB1()));
assertThat(a1_1.getA1_arrayB2(), is(a1_2.getA1_arrayB2()));
assertThat(a1_1.getA1_setBoolean(), is(a1_2.getA1_setBoolean()));
assertThat(a1_1.getA1_setChar(), is(a1_2.getA1_setChar()));
assertThat(a1_1.getA1_setByte(), is(a1_2.getA1_setByte()));
assertThat(a1_1.getA1_setShort(), is(a1_2.getA1_setShort()));
assertThat(a1_1.getA1_setInteger(), is(a1_2.getA1_setInteger()));
assertThat(a1_1.getA1_setLong(), is(a1_2.getA1_setLong()));
assertThat(a1_1.getA1_setFloat(), is(a1_2.getA1_setFloat()));
assertThat(a1_1.getA1_setDouble(), is(a1_2.getA1_setDouble()));
assertThat(a1_1.getA1_setString(), is(a1_2.getA1_setString()));
assertThat(a1_1.getA1_setBigInteger(), is(a1_2.getA1_setBigInteger()));
assertThat(a1_1.getA1_setBigDecimal(), is(a1_2.getA1_setBigDecimal()));
assertThat(a1_1.getA1_setLocalDate(), is(a1_2.getA1_setLocalDate()));
assertThat(a1_1.getA1_setLocalTime(), is(a1_2.getA1_setLocalTime()));
assertThat(a1_1.getA1_setLocalDateTime(), is(a1_2.getA1_setLocalDateTime()));
assertThat(a1_1.getA1_setEnm(), is(a1_2.getA1_setEnm()));
assertThat(a1_1.getA1_setB1(), is(a1_2.getA1_setB1()));
assertThat(a1_1.getA1_setB2(), is(a1_2.getA1_setB2()));
assertThat(a1_1.getA1_mapBooleanBoolean(), is(a1_2.getA1_mapBooleanBoolean()));
assertThat(a1_1.getA1_mapCharChar(), is(a1_2.getA1_mapCharChar()));
assertThat(a1_1.getA1_mapByteByte(), is(a1_2.getA1_mapByteByte()));
assertThat(a1_1.getA1_mapShortShort(), is(a1_2.getA1_mapShortShort()));
assertThat(a1_1.getA1_mapIntegerInteger(), is(a1_2.getA1_mapIntegerInteger()));
assertThat(a1_1.getA1_mapLongLong(), is(a1_2.getA1_mapLongLong()));
assertThat(a1_1.getA1_mapFloatFloat(), is(a1_2.getA1_mapFloatFloat()));
assertThat(a1_1.getA1_mapDoubleDouble(), is(a1_2.getA1_mapDoubleDouble()));
assertThat(a1_1.getA1_mapStringString(), is(a1_2.getA1_mapStringString()));
assertThat(a1_1.getA1_mapBigIntegerBigInteger(), is(a1_2.getA1_mapBigIntegerBigInteger()));
assertThat(a1_1.getA1_mapBigDecimalBigDecimal(), is(a1_2.getA1_mapBigDecimalBigDecimal()));
assertThat(a1_1.getA1_mapLocalDateLocalDate(), is(a1_2.getA1_mapLocalDateLocalDate()));
assertThat(a1_1.getA1_mapLocalTimeLocalTime(), is(a1_2.getA1_mapLocalTimeLocalTime()));
assertThat(a1_1.getA1_mapLocalDateTimeLocalDateTime(), is(a1_2.getA1_mapLocalDateTimeLocalDateTime()));
assertThat(a1_1.getA1_mapEnmEnm(), is(a1_2.getA1_mapEnmEnm()));
assertThat(a1_1.getA1_mapIntegerB1(), is(a1_2.getA1_mapIntegerB1()));
assertThat(a1_1.getA1_mapEnmB2(), is(a1_2.getA1_mapEnmB2()));
assertThat(a1_1.getA1_listEmpty(), is(a1_2.getA1_listEmpty()));
assertThat(a1_1.getA1_arrayEmpty(), is(a1_2.getA1_arrayEmpty()));
assertThat(a1_1.getA1_setEmpty(), is(a1_2.getA1_setEmpty()));
assertThat(a1_1.getA1_mapEmpty(), is(a1_2.getA1_mapEmpty()));
assertThat(a1_1.getA1_listListByte(), is(a1_2.getA1_listListByte()));
assertDeepEquals(a1_1.getA1_listArrayFloat(), a1_2.getA1_listArrayFloat(), ArrayList::new);
assertThat(a1_1.getA1_listSetString(), is(a1_2.getA1_listSetString()));
assertThat(a1_1.getA1_listMapEnmLocalDate(), is(a1_2.getA1_listMapEnmLocalDate()));
assertThat(a1_1.getA1_setSetShort(), is(a1_2.getA1_setSetShort()));
assertDeepEquals(a1_1.getA1_setArrayDouble(), a1_2.getA1_setArrayDouble(), HashSet::new);
assertThat(a1_1.getA1_setListString(), is(a1_2.getA1_setListString()));
assertThat(a1_1.getA1_setMapEnmLocalTime(), is(a1_2.getA1_setMapEnmLocalTime()));
assertThat(a1_1.getA1_mapIntegerMapLongBoolean(), is(a1_2.getA1_mapIntegerMapLongBoolean()));
assertThat(a1_1.getA1_mapStringListB1(), is(a1_2.getA1_mapStringListB1()));
assertEquals(a1_2.getA1_mapBigIntegerArrayBigDecimal().keySet(), a1_1.getA1_mapBigIntegerArrayBigDecimal().keySet());
assertDeepEquals(a1_2.getA1_mapBigIntegerArrayBigDecimal().values(), a1_2.getA1_mapBigIntegerArrayBigDecimal().values(), HashSet::new);
assertThat(a1_1.getA1_mapEnmSetB2(), is(a1_2.getA1_mapEnmSetB2()));
assertThat(a1_1.getA1_mapIntegerListMapShortSetB2(), is(a1_2.getA1_mapIntegerListMapShortSetB2()));
assertThat(a1_1.getA1_arrayArrayPrimBoolean(), is(a1_2.getA1_arrayArrayPrimBoolean()));
assertThat(a1_1.getA1_arrayArrayPrimChar(), is(a1_2.getA1_arrayArrayPrimChar()));
assertThat(a1_1.getA1_arrayArrayPrimByte(), is(a1_2.getA1_arrayArrayPrimByte()));
assertThat(a1_1.getA1_arrayArrayPrimShort(), is(a1_2.getA1_arrayArrayPrimShort()));
assertThat(a1_1.getA1_arrayArrayPrimInteger(), is(a1_2.getA1_arrayArrayPrimInteger()));
assertThat(a1_1.getA1_arrayArrayPrimLong(), is(a1_2.getA1_arrayArrayPrimLong()));
assertThat(a1_1.getA1_arrayArrayPrimFloat(), is(a1_2.getA1_arrayArrayPrimFloat()));
assertThat(a1_1.getA1_arrayArrayPrimDouble(), is(a1_2.getA1_arrayArrayPrimDouble()));
assertThat(a1_1.getA1_arrayArrayBoolean(), is(a1_2.getA1_arrayArrayBoolean()));
assertThat(a1_1.getA1_arrayArrayChar(), is(a1_2.getA1_arrayArrayChar()));
assertThat(a1_1.getA1_arrayArrayByte(), is(a1_2.getA1_arrayArrayByte()));
assertThat(a1_1.getA1_arrayArrayShort(), is(a1_2.getA1_arrayArrayShort()));
assertThat(a1_1.getA1_arrayArrayInteger(), is(a1_2.getA1_arrayArrayInteger()));
assertThat(a1_1.getA1_arrayArrayLong(), is(a1_2.getA1_arrayArrayLong()));
assertThat(a1_1.getA1_arrayArrayFloat(), is(a1_2.getA1_arrayArrayFloat()));
assertThat(a1_1.getA1_arrayArrayDouble(), is(a1_2.getA1_arrayArrayDouble()));
assertThat(a1_1.getA1_arrayArrayString(), is(a1_2.getA1_arrayArrayString()));
assertThat(a1_1.getA1_arrayArrayBigInteger(), is(a1_2.getA1_arrayArrayBigInteger()));
assertThat(a1_1.getA1_arrayArrayBigDecimal(), is(a1_2.getA1_arrayArrayBigDecimal()));
assertThat(a1_1.getA1_arrayArrayLocalDate(), is(a1_2.getA1_arrayArrayLocalDate()));
assertThat(a1_1.getA1_arrayArrayLocalTime(), is(a1_2.getA1_arrayArrayLocalTime()));
assertThat(a1_1.getA1_arrayArrayLocalDateTime(), is(a1_2.getA1_arrayArrayLocalDateTime()));
assertThat(a1_1.getA1_arrayArrayEnm(), is(a1_2.getA1_arrayArrayEnm()));
assertThat(a1_1.getA1_arrayArrayB1(), is(a1_2.getA1_arrayArrayB1()));
assertThat(a1_1.getA1_arrayArrayB2(), is(a1_2.getA1_arrayArrayB2()));
assertThat(a1_1.getA1_point(), is(a1_2.getA1_point()));
assertThat(a1_1.getA1_listPoint(), is(a1_2.getA1_listPoint()));
assertThat(a1_1.getA1_arrayPoint(), is(a1_2.getA1_arrayPoint()));
assertThat(a1_1.getA1_setPoint(), is(a1_2.getA1_setPoint()));
assertThat(a1_1.getA1_mapEnmListPoint(), is(a1_2.getA1_mapEnmListPoint()));
}
static void assertExampleConfigurationsA2Equal(
ExampleConfigurationA2 a2_1,
ExampleConfigurationA2 a2_2
) {
assertThat(ExampleConfigurationA2.getA1_staticFinalInt(), is(1));
assertThat(ExampleConfigurationA2.getA1_staticInt(), is(2));
assertExampleConfigurationsA1Equal(a2_1, a2_2);
assertThat(a2_1.getA2_finalInt(), is(a2_2.getA2_finalInt()));
assertThat(a2_1.getA2_transientInt(), is(a2_2.getA2_transientInt()));
assertThat(a2_1.getA2_ignoredInt(), is(a2_2.getA2_ignoredInt()));
assertThat(a2_1.getA2_ignoredString(), is(a2_2.getA2_ignoredString()));
assertThat(a2_1.getA2_ignoredListString(), is(a2_2.getA2_ignoredListString()));
assertThat(a2_1.isA2_primBool(), is(a2_2.isA2_primBool()));
assertThat(a2_1.getA2_primChar(), is(a2_2.getA2_primChar()));
assertThat(a2_1.getA2_primByte(), is(a2_2.getA2_primByte()));
assertThat(a2_1.getA2_primShort(), is(a2_2.getA2_primShort()));
assertThat(a2_1.getA2_primInt(), is(a2_2.getA2_primInt()));
assertThat(a2_1.getA2_primLong(), is(a2_2.getA2_primLong()));
assertThat(a2_1.getA2_primFloat(), is(a2_2.getA2_primFloat()));
assertThat(a2_1.getA2_primDouble(), is(a2_2.getA2_primDouble()));
assertThat(a2_1.getA2_refBool(), is(a2_2.getA2_refBool()));
assertThat(a2_1.getA2_refChar(), is(a2_2.getA2_refChar()));
assertThat(a2_1.getA2_refByte(), is(a2_2.getA2_refByte()));
assertThat(a2_1.getA2_refShort(), is(a2_2.getA2_refShort()));
assertThat(a2_1.getA2_refInt(), is(a2_2.getA2_refInt()));
assertThat(a2_1.getA2_refLong(), is(a2_2.getA2_refLong()));
assertThat(a2_1.getA2_refFloat(), is(a2_2.getA2_refFloat()));
assertThat(a2_1.getA2_refDouble(), is(a2_2.getA2_refDouble()));
assertThat(a2_1.getA2_string(), is(a2_2.getA2_string()));
assertThat(a2_1.getA2_bigInteger(), is(a2_2.getA2_bigInteger()));
assertThat(a2_1.getA2_bigDecimal(), is(a2_2.getA2_bigDecimal()));
assertThat(a2_1.getA2_localDate(), is(a2_2.getA2_localDate()));
assertThat(a2_1.getA2_localTime(), is(a2_2.getA2_localTime()));
assertThat(a2_1.getA2_localDateTime(), is(a2_2.getA2_localDateTime()));
assertThat(a2_1.getA2_Enm(), is(a2_2.getA2_Enm()));
assertThat(a2_1.getA2_b1(), is(a2_2.getA2_b1()));
assertThat(a2_1.getA2_b2(), is(a2_2.getA2_b2()));
assertThat(a2_1.getA2_listBoolean(), is(a2_2.getA2_listBoolean()));
assertThat(a2_1.getA2_listChar(), is(a2_2.getA2_listChar()));
assertThat(a2_1.getA2_listByte(), is(a2_2.getA2_listByte()));
assertThat(a2_1.getA2_listShort(), is(a2_2.getA2_listShort()));
assertThat(a2_1.getA2_listInteger(), is(a2_2.getA2_listInteger()));
assertThat(a2_1.getA2_listLong(), is(a2_2.getA2_listLong()));
assertThat(a2_1.getA2_listFloat(), is(a2_2.getA2_listFloat()));
assertThat(a2_1.getA2_listDouble(), is(a2_2.getA2_listDouble()));
assertThat(a2_1.getA2_listString(), is(a2_2.getA2_listString()));
assertThat(a2_1.getA2_listBigInteger(), is(a2_2.getA2_listBigInteger()));
assertThat(a2_1.getA2_listBigDecimal(), is(a2_2.getA2_listBigDecimal()));
assertThat(a2_1.getA2_listLocalDate(), is(a2_2.getA2_listLocalDate()));
assertThat(a2_1.getA2_listLocalTime(), is(a2_2.getA2_listLocalTime()));
assertThat(a2_1.getA2_listLocalDateTime(), is(a2_2.getA2_listLocalDateTime()));
assertThat(a2_1.getA2_listEnm(), is(a2_2.getA2_listEnm()));
assertThat(a2_1.getA2_listB1(), is(a2_2.getA2_listB1()));
assertThat(a2_1.getA2_listB2(), is(a2_2.getA2_listB2()));
assertThat(a2_1.getA2_arrayPrimBoolean(), is(a2_2.getA2_arrayPrimBoolean()));
assertThat(a2_1.getA2_arrayPrimChar(), is(a2_2.getA2_arrayPrimChar()));
assertThat(a2_1.getA2_arrayPrimByte(), is(a2_2.getA2_arrayPrimByte()));
assertThat(a2_1.getA2_arrayPrimShort(), is(a2_2.getA2_arrayPrimShort()));
assertThat(a2_1.getA2_arrayPrimInteger(), is(a2_2.getA2_arrayPrimInteger()));
assertThat(a2_1.getA2_arrayPrimLong(), is(a2_2.getA2_arrayPrimLong()));
assertThat(a2_1.getA2_arrayPrimFloat(), is(a2_2.getA2_arrayPrimFloat()));
assertThat(a2_1.getA2_arrayPrimDouble(), is(a2_2.getA2_arrayPrimDouble()));
assertThat(a2_1.getA2_arrayBoolean(), is(a2_2.getA2_arrayBoolean()));
assertThat(a2_1.getA2_arrayChar(), is(a2_2.getA2_arrayChar()));
assertThat(a2_1.getA2_arrayByte(), is(a2_2.getA2_arrayByte()));
assertThat(a2_1.getA2_arrayShort(), is(a2_2.getA2_arrayShort()));
assertThat(a2_1.getA2_arrayInteger(), is(a2_2.getA2_arrayInteger()));
assertThat(a2_1.getA2_arrayLong(), is(a2_2.getA2_arrayLong()));
assertThat(a2_1.getA2_arrayFloat(), is(a2_2.getA2_arrayFloat()));
assertThat(a2_1.getA2_arrayDouble(), is(a2_2.getA2_arrayDouble()));
assertThat(a2_1.getA2_arrayString(), is(a2_2.getA2_arrayString()));
assertThat(a2_1.getA2_arrayBigInteger(), is(a2_2.getA2_arrayBigInteger()));
assertThat(a2_1.getA2_arrayBigDecimal(), is(a2_2.getA2_arrayBigDecimal()));
assertThat(a2_1.getA2_arrayLocalDate(), is(a2_2.getA2_arrayLocalDate()));
assertThat(a2_1.getA2_arrayLocalTime(), is(a2_2.getA2_arrayLocalTime()));
assertThat(a2_1.getA2_arrayLocalDateTime(), is(a2_2.getA2_arrayLocalDateTime()));
assertThat(a2_1.getA2_arrayEnm(), is(a2_2.getA2_arrayEnm()));
assertThat(a2_1.getA2_arrayB1(), is(a2_2.getA2_arrayB1()));
assertThat(a2_1.getA2_arrayB2(), is(a2_2.getA2_arrayB2()));
assertThat(a2_1.getA2_setBoolean(), is(a2_2.getA2_setBoolean()));
assertThat(a2_1.getA2_setChar(), is(a2_2.getA2_setChar()));
assertThat(a2_1.getA2_setByte(), is(a2_2.getA2_setByte()));
assertThat(a2_1.getA2_setShort(), is(a2_2.getA2_setShort()));
assertThat(a2_1.getA2_setInteger(), is(a2_2.getA2_setInteger()));
assertThat(a2_1.getA2_setLong(), is(a2_2.getA2_setLong()));
assertThat(a2_1.getA2_setFloat(), is(a2_2.getA2_setFloat()));
assertThat(a2_1.getA2_setDouble(), is(a2_2.getA2_setDouble()));
assertThat(a2_1.getA2_setString(), is(a2_2.getA2_setString()));
assertThat(a2_1.getA2_setBigInteger(), is(a2_2.getA2_setBigInteger()));
assertThat(a2_1.getA2_setBigDecimal(), is(a2_2.getA2_setBigDecimal()));
assertThat(a2_1.getA2_setLocalDate(), is(a2_2.getA2_setLocalDate()));
assertThat(a2_1.getA2_setLocalTime(), is(a2_2.getA2_setLocalTime()));
assertThat(a2_1.getA2_setLocalDateTime(), is(a2_2.getA2_setLocalDateTime()));
assertThat(a2_1.getA2_setEnm(), is(a2_2.getA2_setEnm()));
assertThat(a2_1.getA2_setB1(), is(a2_2.getA2_setB1()));
assertThat(a2_1.getA2_setB2(), is(a2_2.getA2_setB2()));
assertThat(a2_1.getA2_mapBooleanBoolean(), is(a2_2.getA2_mapBooleanBoolean()));
assertThat(a2_1.getA2_mapCharChar(), is(a2_2.getA2_mapCharChar()));
assertThat(a2_1.getA2_mapByteByte(), is(a2_2.getA2_mapByteByte()));
assertThat(a2_1.getA2_mapShortShort(), is(a2_2.getA2_mapShortShort()));
assertThat(a2_1.getA2_mapIntegerInteger(), is(a2_2.getA2_mapIntegerInteger()));
assertThat(a2_1.getA2_mapLongLong(), is(a2_2.getA2_mapLongLong()));
assertThat(a2_1.getA2_mapFloatFloat(), is(a2_2.getA2_mapFloatFloat()));
assertThat(a2_1.getA2_mapDoubleDouble(), is(a2_2.getA2_mapDoubleDouble()));
assertThat(a2_1.getA2_mapStringString(), is(a2_2.getA2_mapStringString()));
assertThat(a2_1.getA2_mapBigIntegerBigInteger(), is(a2_2.getA2_mapBigIntegerBigInteger()));
assertThat(a2_1.getA2_mapBigDecimalBigDecimal(), is(a2_2.getA2_mapBigDecimalBigDecimal()));
assertThat(a2_1.getA2_mapLocalDateLocalDate(), is(a2_2.getA2_mapLocalDateLocalDate()));
assertThat(a2_1.getA2_mapLocalTimeLocalTime(), is(a2_2.getA2_mapLocalTimeLocalTime()));
assertThat(a2_1.getA2_mapLocalDateTimeLocalDateTime(), is(a2_2.getA2_mapLocalDateTimeLocalDateTime()));
assertThat(a2_1.getA2_mapEnmEnm(), is(a2_2.getA2_mapEnmEnm()));
assertThat(a2_1.getA2_mapIntegerB1(), is(a2_2.getA2_mapIntegerB1()));
assertThat(a2_1.getA2_mapEnmB2(), is(a2_2.getA2_mapEnmB2()));
assertThat(a2_1.getA2_listEmpty(), is(a2_2.getA2_listEmpty()));
assertThat(a2_1.getA2_arrayEmpty(), is(a2_2.getA2_arrayEmpty()));
assertThat(a2_1.getA2_setEmpty(), is(a2_2.getA2_setEmpty()));
assertThat(a2_1.getA2_mapEmpty(), is(a2_2.getA2_mapEmpty()));
assertThat(a2_1.getA2_listListByte(), is(a2_2.getA2_listListByte()));
assertDeepEquals(a2_1.getA2_listArrayFloat(), a2_2.getA2_listArrayFloat(), ArrayList::new);
assertThat(a2_1.getA2_listSetString(), is(a2_2.getA2_listSetString()));
assertThat(a2_1.getA2_listMapEnmLocalDate(), is(a2_2.getA2_listMapEnmLocalDate()));
assertThat(a2_1.getA2_setSetShort(), is(a2_2.getA2_setSetShort()));
assertDeepEquals(a2_1.getA2_setArrayDouble(), a2_2.getA2_setArrayDouble(), HashSet::new);
assertThat(a2_1.getA2_setListString(), is(a2_2.getA2_setListString()));
assertThat(a2_1.getA2_setMapEnmLocalTime(), is(a2_2.getA2_setMapEnmLocalTime()));
assertThat(a2_1.getA2_mapIntegerMapLongBoolean(), is(a2_2.getA2_mapIntegerMapLongBoolean()));
assertThat(a2_1.getA2_mapStringListB1(), is(a2_2.getA2_mapStringListB1()));
assertEquals(a2_2.getA2_mapBigIntegerArrayBigDecimal().keySet(), a2_1.getA2_mapBigIntegerArrayBigDecimal().keySet());
assertDeepEquals(a2_2.getA2_mapBigIntegerArrayBigDecimal().values(), a2_2.getA2_mapBigIntegerArrayBigDecimal().values(), HashSet::new);
assertThat(a2_1.getA2_mapEnmSetB2(), is(a2_2.getA2_mapEnmSetB2()));
assertThat(a2_1.getA2_mapIntegerListMapShortSetB2(), is(a2_2.getA2_mapIntegerListMapShortSetB2()));
assertThat(a2_1.getA2_arrayArrayPrimBoolean(), is(a2_2.getA2_arrayArrayPrimBoolean()));
assertThat(a2_1.getA2_arrayArrayPrimChar(), is(a2_2.getA2_arrayArrayPrimChar()));
assertThat(a2_1.getA2_arrayArrayPrimByte(), is(a2_2.getA2_arrayArrayPrimByte()));
assertThat(a2_1.getA2_arrayArrayPrimShort(), is(a2_2.getA2_arrayArrayPrimShort()));
assertThat(a2_1.getA2_arrayArrayPrimInteger(), is(a2_2.getA2_arrayArrayPrimInteger()));
assertThat(a2_1.getA2_arrayArrayPrimLong(), is(a2_2.getA2_arrayArrayPrimLong()));
assertThat(a2_1.getA2_arrayArrayPrimFloat(), is(a2_2.getA2_arrayArrayPrimFloat()));
assertThat(a2_1.getA2_arrayArrayPrimDouble(), is(a2_2.getA2_arrayArrayPrimDouble()));
assertThat(a2_1.getA2_arrayArrayBoolean(), is(a2_2.getA2_arrayArrayBoolean()));
assertThat(a2_1.getA2_arrayArrayChar(), is(a2_2.getA2_arrayArrayChar()));
assertThat(a2_1.getA2_arrayArrayByte(), is(a2_2.getA2_arrayArrayByte()));
assertThat(a2_1.getA2_arrayArrayShort(), is(a2_2.getA2_arrayArrayShort()));
assertThat(a2_1.getA2_arrayArrayInteger(), is(a2_2.getA2_arrayArrayInteger()));
assertThat(a2_1.getA2_arrayArrayLong(), is(a2_2.getA2_arrayArrayLong()));
assertThat(a2_1.getA2_arrayArrayFloat(), is(a2_2.getA2_arrayArrayFloat()));
assertThat(a2_1.getA2_arrayArrayDouble(), is(a2_2.getA2_arrayArrayDouble()));
assertThat(a2_1.getA2_arrayArrayString(), is(a2_2.getA2_arrayArrayString()));
assertThat(a2_1.getA2_arrayArrayBigInteger(), is(a2_2.getA2_arrayArrayBigInteger()));
assertThat(a2_1.getA2_arrayArrayBigDecimal(), is(a2_2.getA2_arrayArrayBigDecimal()));
assertThat(a2_1.getA2_arrayArrayLocalDate(), is(a2_2.getA2_arrayArrayLocalDate()));
assertThat(a2_1.getA2_arrayArrayLocalTime(), is(a2_2.getA2_arrayArrayLocalTime()));
assertThat(a2_1.getA2_arrayArrayLocalDateTime(), is(a2_2.getA2_arrayArrayLocalDateTime()));
assertThat(a2_1.getA2_arrayArrayEnm(), is(a2_2.getA2_arrayArrayEnm()));
assertThat(a2_1.getA2_arrayArrayB1(), is(a2_2.getA2_arrayArrayB1()));
assertThat(a2_1.getA2_arrayArrayB2(), is(a2_2.getA2_arrayArrayB2()));
assertThat(a2_1.getA2_point(), is(a2_2.getA2_point()));
assertThat(a2_1.getA2_listPoint(), is(a2_2.getA2_listPoint()));
assertThat(a2_1.getA2_arrayPoint(), is(a2_2.getA2_arrayPoint()));
assertThat(a2_1.getA2_setPoint(), is(a2_2.getA2_setPoint()));
assertThat(a2_1.getA2_mapEnmListPoint(), is(a2_2.getA2_mapEnmListPoint()));
}
private static void assertExampleConfigurationsB1Equal(
ExampleConfigurationB1 b1_1,
ExampleConfigurationB1 b1_2
) {
assertThat(ExampleConfigurationB1.getB1_staticFinalInt(), is(1));
assertThat(ExampleConfigurationB1.getB1_staticInt(), is(2));
assertThat(b1_1.getB1_finalInt(), is(b1_2.getB1_finalInt()));
assertThat(b1_1.getB1_transientInt(), is(b1_2.getB1_transientInt()));
assertThat(b1_1.getB1_ignoredInt(), is(b1_2.getB1_ignoredInt()));
assertThat(b1_1.getB1_ignoredString(), is(b1_2.getB1_ignoredString()));
assertThat(b1_1.getB1_ignoredListString(), is(b1_2.getB1_ignoredListString()));
assertThat(b1_1.isB1_primBool(), is(b1_2.isB1_primBool()));
assertThat(b1_1.getB1_refChar(), is(b1_2.getB1_refChar()));
assertThat(b1_1.getB1_string(), is(b1_2.getB1_string()));
assertThat(b1_1.getB1_listByte(), is(b1_2.getB1_listByte()));
assertThat(b1_1.getB1_arrayShort(), is(b1_2.getB1_arrayShort()));
assertThat(b1_1.getB1_setInteger(), is(b1_2.getB1_setInteger()));
assertThat(b1_1.getB1_listEmpty(), is(b1_2.getB1_listEmpty()));
assertThat(b1_1.getB1_mapLongLong(), is(b1_2.getB1_mapLongLong()));
assertThat(b1_1.getB1_listListByte(), is(b1_2.getB1_listListByte()));
assertThat(b1_1.getB1_point(), is(b1_2.getB1_point()));
assertEquals(b1_2, b1_1);
}
private static void assertExampleConfigurationsB2Equal(
ExampleConfigurationB2 b2_1,
ExampleConfigurationB2 b2_2
) {
assertExampleConfigurationsB1Equal(b2_1, b2_2);
assertThat(ExampleConfigurationB2.getB1_staticFinalInt(), is(1));
assertThat(ExampleConfigurationB2.getB1_staticInt(), is(2));
assertThat(b2_1.getB2_finalInt(), is(b2_2.getB2_finalInt()));
assertThat(b2_1.getB2_transientInt(), is(b2_2.getB2_transientInt()));
assertThat(b2_1.getB2_ignoredInt(), is(b2_2.getB2_ignoredInt()));
assertThat(b2_1.getB2_ignoredString(), is(b2_2.getB2_ignoredString()));
assertThat(b2_1.getB2_ignoredListString(), is(b2_2.getB2_ignoredListString()));
assertThat(b2_1.getB2_primChar(), is(b2_2.getB2_primChar()));
assertThat(b2_1.getB2_refBool(), is(b2_2.getB2_refBool()));
assertThat(b2_1.getB2_bigInteger(), is(b2_2.getB2_bigInteger()));
assertThat(b2_1.getB2_listShort(), is(b2_2.getB2_listShort()));
assertThat(b2_1.getB2_arrayInteger(), is(b2_2.getB2_arrayInteger()));
assertThat(b2_1.getB2_setLong(), is(b2_2.getB2_setLong()));
assertThat(b2_1.getB2_arrayEmpty(), is(b2_2.getB2_arrayEmpty()));
assertThat(b2_1.getB2_mapFloatFloat(), is(b2_2.getB2_mapFloatFloat()));
assertDeepEquals(b2_1.getB2_setArrayDouble(), b2_2.getB2_setArrayDouble(), LinkedHashSet::new);
assertThat(b2_1.getB2_listPoint(), is(b2_2.getB2_listPoint()));
assertEquals(b2_2, b2_1);
}
private static <T, C extends Collection<T[]>> void assertDeepEquals(
C collection1,
C collection2,
Supplier<Collection<List<T>>> collectionFactory
) {
Collection<List<T>> c1 = collection1.stream().map(Arrays::asList)
.collect(Collectors.toCollection(collectionFactory));
Collection<List<T>> c2 = collection2.stream().map(Arrays::asList)
.collect(Collectors.toCollection(collectionFactory));
assertEquals(c2, c1);
}
@Test
void yamlStoreSavesAndLoadsExampleConfigurationA2() {
var properties = YamlConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER)
.build();
var store = new YamlConfigurationStore<>(ExampleConfigurationA2.class, properties);
ExampleConfigurationA2 cfg1 = ExampleInitializer.newExampleConfigurationA2();
store.save(cfg1, yamlFile);
ExampleConfigurationA2 cfg2 = store.load(yamlFile);
assertExampleConfigurationsA2Equal(cfg1, cfg2);
}
@Test
void yamlStoreSavesAndLoadsExampleConfigurationNullsWithNullCollectionElements1() {
var properties = YamlConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER)
.outputNulls(true)
.inputNulls(true)
.build();
var store = new YamlConfigurationStore<>(ExampleConfigurationNulls.class, properties);
ExampleConfigurationNulls cfg1 = ExampleInitializer
.newExampleConfigurationNullsWithNullCollectionElements1();
store.save(cfg1, yamlFile);
ExampleConfigurationNulls cfg2 = store.load(yamlFile);
assertExampleConfigurationsNullsEqual(cfg1, cfg2);
}
@Test
void yamlStoreSavesAndLoadsExampleConfigurationNullsWithoutNullCollectionElements1() {
var properties = YamlConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_SERIALIZER)
.build();
var store = new YamlConfigurationStore<>(ExampleConfigurationNulls.class, properties);
ExampleConfigurationNulls cfg1 = ExampleInitializer
.newExampleConfigurationNullsWithoutNullCollectionElements1();
store.save(cfg1, yamlFile);
ExampleConfigurationNulls cfg2 = store.load(yamlFile);
assertExampleConfigurationsNullsEqual(cfg1, cfg2);
}
}

@ -0,0 +1,16 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
class FieldExtractorTest {
@Test
void applyCallsExtract() {
FieldExtractor extractor = mock(FieldExtractor.class, CALLS_REAL_METHODS);
extractor.apply(Object.class);
verify(extractor).extract(Object.class);
}
}

@ -0,0 +1,102 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.List;
import static de.exlll.configlib.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class FieldExtractorsTest {
@Test
void extractConfigurationRequiresNonNull() {
assertThrowsNullPointerException(
() -> FieldExtractors.CONFIGURATION.extract(null),
"configuration class"
);
}
@Test
void extractConfigurationRequiresConfiguration() {
assertThrowsConfigurationException(
() -> FieldExtractors.CONFIGURATION.extract(getClass()),
"Class 'FieldExtractorsTest' must be a configuration."
);
}
@Test
void extractConfigurationDirectly() {
@Configuration
class A {
int i;
}
FieldExtractor extractor = FieldExtractors.CONFIGURATION;
List<Field> actual = extractor.extract(A.class).toList();
List<Field> expected = List.of(
getField(A.class, "i")
);
assertThat(actual, is(expected));
}
@Test
void extractConfigurationIndirectly() {
@Configuration
class A {
int i;
}
class B extends A {
int j;
}
class C extends B {
int k;
}
FieldExtractor extractor = FieldExtractors.CONFIGURATION;
List<Field> actual = extractor.extract(C.class).toList();
List<Field> expected = List.of(
getField(A.class, "i"),
getField(B.class, "j"),
getField(C.class, "k")
);
assertThat(actual, is(expected));
}
@Test
void extractConfigurationRequiresNoShadowing() {
@Configuration
class A {
int i;
}
class B extends A {
int i;
}
assertThrowsConfigurationException(
() -> FieldExtractors.CONFIGURATION.extract(B.class),
"Shadowing of fields is not supported. Field 'i' of class B " +
"shadows field 'i' of class A."
);
}
@Test
void extractConfigurationAppliesDefaultFieldFilter() {
class A {
@Configuration
class B {
private final int i = 1;
@Ignore
private int j;
private static int k;
private transient int l;
private int m;
}
}
List<Field> actual = FieldExtractors.CONFIGURATION.extract(A.B.class).toList();
List<Field> expected = List.of(getField(A.B.class, "m"));
assertThat(actual, is(expected));
}
}

@ -0,0 +1,41 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import static de.exlll.configlib.TestUtils.assertThrowsNullPointerException;
import static de.exlll.configlib.TestUtils.getField;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class FieldFiltersTest {
@Test
void filterDefaultRequiresNonNullStream() {
assertThrowsNullPointerException(
() -> FieldFilters.DEFAULT.test(null),
"field"
);
}
@Test
void filterDefault() {
class A {
class B {
private final int i = 1;
@Ignore
private int j;
private static int k;
private transient int l;
private int m;
}
}
List<Field> actual = Arrays.stream(A.B.class.getDeclaredFields())
.filter(FieldFilters.DEFAULT)
.toList();
List<Field> expected = List.of(getField(A.B.class, "m"));
assertThat(actual, is(expected));
}
}

@ -0,0 +1,22 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import static org.mockito.Mockito.*;
class FieldFormatterTest {
@Test
void applyCallsFormat() {
class A {
int i;
}
FieldFormatter formatter = mock(FieldFormatter.class, CALLS_REAL_METHODS);
Field field = TestUtils.getField(A.class, "i");
formatter.apply(field);
verify(formatter).format(field);
}
}

@ -0,0 +1,58 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import static de.exlll.configlib.TestUtils.getField;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
class FieldFormattersTest {
private static class A {
String lowercase;
String camelCase;
String withNumber123;
String with123Number;
String with_$;
}
private static final Field f1 = getField(A.class, "lowercase");
private static final Field f2 = getField(A.class, "camelCase");
private static final Field f3 = getField(A.class, "withNumber123");
private static final Field f4 = getField(A.class, "with123Number");
private static final Field f5 = getField(A.class, "with_$");
@Test
void formatIdentity() {
FieldFormatters formatter = FieldFormatters.IDENTITY;
assertThat(formatter.format(f1), is("lowercase"));
assertThat(formatter.format(f2), is("camelCase"));
assertThat(formatter.format(f3), is("withNumber123"));
assertThat(formatter.format(f4), is("with123Number"));
assertThat(formatter.format(f5), is("with_$"));
}
@Test
void formatLowerUnderscore() {
FieldFormatters formatter = FieldFormatters.LOWER_UNDERSCORE;
assertThat(formatter.format(f1), is("lowercase"));
assertThat(formatter.format(f2), is("camel_case"));
assertThat(formatter.format(f3), is("with_number123"));
assertThat(formatter.format(f4), is("with123_number"));
assertThat(formatter.format(f5), is("with_$"));
}
@Test
void formatUpperUnderscore() {
FieldFormatters formatter = FieldFormatters.UPPER_UNDERSCORE;
assertThat(formatter.format(f1), is("LOWERCASE"));
assertThat(formatter.format(f2), is("CAMEL_CASE"));
assertThat(formatter.format(f3), is("WITH_NUMBER123"));
assertThat(formatter.format(f4), is("WITH123_NUMBER"));
assertThat(formatter.format(f5), is("WITH_$"));
}
}

@ -0,0 +1,27 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FileConfigurationPropertiesTest {
@Test
void builderDefaultValues() {
FileConfigurationProperties properties = FileConfigurationProperties.newBuilder().build();
assertNull(properties.getHeader());
assertNull(properties.getFooter());
assertTrue(properties.createParentDirectories());
}
@Test
void builderCopiesValues() {
FileConfigurationProperties properties = FileConfigurationProperties.newBuilder()
.header("THE HEADER")
.footer("THE FOOTER")
.createParentDirectories(false)
.build();
assertEquals("THE HEADER", properties.getHeader());
assertEquals("THE FOOTER", properties.getFooter());
assertFalse(properties.createParentDirectories());
}
}

@ -0,0 +1,217 @@
package de.exlll.configlib;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import static de.exlll.configlib.TestUtils.assertThrowsRuntimeException;
import static de.exlll.configlib.TestUtils.getField;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
class ReflectTest {
static class B1 {
B1(int i) {}
}
@Test
void newInstanceRequiresNoArgsCtor() {
assertThrowsRuntimeException(
() -> Reflect.newInstance(B1.class),
"Class B1 doesn't have a no-args constructor."
);
}
static abstract class B2 {}
@Test
void newInstanceRequiresConcreteClass() {
assertThrowsRuntimeException(
() -> Reflect.newInstance(B2.class),
"Class B2 is not instantiable."
);
}
static class B3 {
B3() {
throw new RuntimeException();
}
}
@Test
void newInstanceRequiresNonThrowingCtor() {
assertThrowsRuntimeException(
() -> Reflect.newInstance(B3.class),
"Constructor of class B3 threw an exception."
);
}
static class B4 {
int i = 10;
}
@Test
void newInstance() {
B4 inst = Reflect.newInstance(B4.class);
assertThat(inst, notNullValue());
assertThat(inst.i, is(10));
}
@ParameterizedTest
@ValueSource(ints = {0, 1, 2})
void newArray(int arrayLength) {
String[] strings = Reflect.newArray(String.class, arrayLength);
assertThat(strings.length, is(arrayLength));
int[][] ints = Reflect.newArray(int[].class, arrayLength);
assertThat(ints.length, is(arrayLength));
}
@Test
void getValue() {
class A {
private int i = 10;
}
int value = (int) Reflect.getValue(getField(A.class, "i"), new A());
assertThat(value, is(10));
}
@Test
void setValue() {
class A {
private int i = 10;
}
A a = new A();
Reflect.setValue(getField(A.class, "i"), a, 20);
assertThat(a.i, is(20));
}
@Test
void setValueDoesNotSetFinalField() {
class A {
private final int i = 10;
}
A a = new A();
Reflect.setValue(getField(A.class, "i"), a, 20);
assertThat(a.i, is(10));
}
@Test
void isIntegerType() {
assertThat(Reflect.isIntegerType(byte.class), is(true));
assertThat(Reflect.isIntegerType(Byte.class), is(true));
assertThat(Reflect.isIntegerType(short.class), is(true));
assertThat(Reflect.isIntegerType(Short.class), is(true));
assertThat(Reflect.isIntegerType(int.class), is(true));
assertThat(Reflect.isIntegerType(Integer.class), is(true));
assertThat(Reflect.isIntegerType(long.class), is(true));
assertThat(Reflect.isIntegerType(Long.class), is(true));
assertThat(Reflect.isIntegerType(float.class), is(false));
assertThat(Reflect.isIntegerType(Float.class), is(false));
assertThat(Reflect.isIntegerType(double.class), is(false));
assertThat(Reflect.isIntegerType(Double.class), is(false));
assertThat(Reflect.isIntegerType(String.class), is(false));
assertThat(Reflect.isIntegerType(Object.class), is(false));
}
@Test
void isFloatingPointType() {
assertThat(Reflect.isFloatingPointType(byte.class), is(false));
assertThat(Reflect.isFloatingPointType(Byte.class), is(false));
assertThat(Reflect.isFloatingPointType(short.class), is(false));
assertThat(Reflect.isFloatingPointType(Short.class), is(false));
assertThat(Reflect.isFloatingPointType(int.class), is(false));
assertThat(Reflect.isFloatingPointType(Integer.class), is(false));
assertThat(Reflect.isFloatingPointType(long.class), is(false));
assertThat(Reflect.isFloatingPointType(Long.class), is(false));
assertThat(Reflect.isFloatingPointType(float.class), is(true));
assertThat(Reflect.isFloatingPointType(Float.class), is(true));
assertThat(Reflect.isFloatingPointType(double.class), is(true));
assertThat(Reflect.isFloatingPointType(Double.class), is(true));
assertThat(Reflect.isFloatingPointType(String.class), is(false));
assertThat(Reflect.isFloatingPointType(Object.class), is(false));
}
@Test
void isEnumType() {
enum A {}
class B {}
assertThat(Reflect.isEnumType(A.class), is(true));
assertThat(Reflect.isEnumType(B.class), is(false));
}
@Test
void isArrayType() {
enum A {}
class B {}
assertThat(Reflect.isArrayType(A.class), is(false));
assertThat(Reflect.isArrayType(B.class), is(false));
assertThat(Reflect.isArrayType(int.class), is(false));
assertThat(Reflect.isArrayType(A[].class), is(true));
assertThat(Reflect.isArrayType(B[].class), is(true));
assertThat(Reflect.isArrayType(int[].class), is(true));
assertThat(Reflect.isArrayType(A[][].class), is(true));
assertThat(Reflect.isArrayType(B[][].class), is(true));
assertThat(Reflect.isArrayType(int[][].class), is(true));
}
@Test
void isListType() {
assertThat(Reflect.isListType(Object.class), is(false));
assertThat(Reflect.isListType(HashMap.class), is(false));
assertThat(Reflect.isListType(HashSet.class), is(false));
assertThat(Reflect.isListType(ArrayList.class), is(true));
}
@Test
void isSetType() {
assertThat(Reflect.isSetType(Object.class), is(false));
assertThat(Reflect.isSetType(HashMap.class), is(false));
assertThat(Reflect.isSetType(HashSet.class), is(true));
assertThat(Reflect.isSetType(ArrayList.class), is(false));
}
@Test
void isMapType() {
assertThat(Reflect.isMapType(Object.class), is(false));
assertThat(Reflect.isMapType(HashMap.class), is(true));
assertThat(Reflect.isMapType(HashSet.class), is(false));
assertThat(Reflect.isMapType(ArrayList.class), is(false));
}
@Test
void isConfiguration() {
class A {}
class B extends A {}
@Configuration
class C {}
class D extends C {}
assertThat(Reflect.isConfiguration(A.class), is(false));
assertThat(Reflect.isConfiguration(B.class), is(false));
assertThat(Reflect.isConfiguration(C.class), is(true));
assertThat(Reflect.isConfiguration(D.class), is(true));
}
@Test
void isIgnored() {
class A {
@Ignore
private int a;
private int b;
}
Field fieldA = getField(A.class, "a");
Field fieldB = getField(A.class, "b");
assertThat(Reflect.isIgnored(fieldA), is(true));
assertThat(Reflect.isIgnored(fieldB), is(false));
}
}

@ -0,0 +1,362 @@
package de.exlll.configlib;
import de.exlll.configlib.Serializers.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.awt.Point;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static de.exlll.configlib.TestUtils.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class SerializerSelectorTest {
private static final SerializerSelector SELECTOR = new SerializerSelector(
ConfigurationProperties.newBuilder().build()
);
private static Type getGenericType(Class<?> cls, String fieldName) {
Field ls = getField(cls, fieldName);
return ls.getGenericType();
}
@ParameterizedTest
@ValueSource(classes = {boolean.class, Boolean.class})
void selectSerializerBoolean(Class<?> cls) {
Serializer<?, ?> serializer = SELECTOR.select(cls);
assertThat(serializer, instanceOf(BooleanSerializer.class));
}
@ParameterizedTest
@ValueSource(classes = {
byte.class, Byte.class, short.class, Short.class,
int.class, Integer.class, long.class, Long.class,
float.class, Float.class, double.class, Double.class
})
void selectSerializerNumber(Class<?> cls) {
NumberSerializer serializer = (NumberSerializer) SELECTOR.select(cls);
assertThat(serializer.getNumberClass(), equalTo(cls));
}
@ParameterizedTest
@ValueSource(classes = {char.class, Character.class})
void selectSerializerChar(Class<?> cls) {
Serializer<?, ?> serializer = SELECTOR.select(cls);
assertThat(serializer, instanceOf(CharacterSerializer.class));
}
@Test
void selectSerializerString() {
Serializer<?, ?> serializer = SELECTOR.select(String.class);
assertThat(serializer, instanceOf(StringSerializer.class));
}
@Test
void selectSerializerBigInteger() {
Serializer<?, ?> serializer = SELECTOR.select(BigInteger.class);
assertThat(serializer, instanceOf(BigIntegerSerializer.class));
}
@Test
void selectSerializerBigDecimal() {
Serializer<?, ?> serializer = SELECTOR.select(BigDecimal.class);
assertThat(serializer, instanceOf(BigDecimalSerializer.class));
}
@Test
void selectSerializerLocalDate() {
Serializer<?, ?> serializer = SELECTOR.select(LocalDate.class);
assertThat(serializer, instanceOf(LocalDateSerializer.class));
}
@Test
void selectSerializerLocalTime() {
Serializer<?, ?> serializer = SELECTOR.select(LocalTime.class);
assertThat(serializer, instanceOf(LocalTimeSerializer.class));
}
@Test
void selectSerializerLocalDateTime() {
Serializer<?, ?> serializer = SELECTOR.select(LocalDateTime.class);
assertThat(serializer, instanceOf(LocalDateTimeSerializer.class));
}
@Test
void selectSerializerEnum() {
enum E {}
EnumSerializer serializer = (EnumSerializer) SELECTOR.select(E.class);
assertThat(serializer.getEnumCls(), equalTo(E.class));
}
@Test
void selectSerializerArray() {
var serializer = (ArraySerializer<?, ?>) SELECTOR.select(String[][].class);
assertThat(serializer.getComponentType(), equalTo(String[].class));
var elementSerializer = (ArraySerializer<?, ?>) serializer.getElementSerializer();
assertThat(elementSerializer.getComponentType(), equalTo(String.class));
assertThat(elementSerializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerPrimitiveBooleanArray() {
Serializer<?, ?> serializer = SELECTOR.select(boolean[].class);
assertThat(serializer, instanceOf(PrimitiveBooleanArraySerializer.class));
}
@Test
void selectSerializerPrimitiveCharacterArray() {
Serializer<?, ?> serializer = SELECTOR.select(char[].class);
assertThat(serializer, instanceOf(PrimitiveCharacterArraySerializer.class));
}
@Test
void selectSerializerPrimitiveByteArray() {
Serializer<?, ?> serializer = SELECTOR.select(byte[].class);
assertThat(serializer, instanceOf(PrimitiveByteArraySerializer.class));
}
@Test
void selectSerializerPrimitiveShortArray() {
Serializer<?, ?> serializer = SELECTOR.select(short[].class);
assertThat(serializer, instanceOf(PrimitiveShortArraySerializer.class));
}
@Test
void selectSerializerPrimitiveIntegerArray() {
Serializer<?, ?> serializer = SELECTOR.select(int[].class);
assertThat(serializer, instanceOf(PrimitiveIntegerArraySerializer.class));
}
@Test
void selectSerializerPrimitiveLongArray() {
Serializer<?, ?> serializer = SELECTOR.select(long[].class);
assertThat(serializer, instanceOf(PrimitiveLongArraySerializer.class));
}
@Test
void selectSerializerPrimitiveFloatArray() {
Serializer<?, ?> serializer = SELECTOR.select(float[].class);
assertThat(serializer, instanceOf(PrimitiveFloatArraySerializer.class));
}
@Test
void selectSerializerPrimitiveDoubleArray() {
Serializer<?, ?> serializer = SELECTOR.select(double[].class);
assertThat(serializer, instanceOf(PrimitiveDoubleArraySerializer.class));
}
@Test
void selectSerializerConfiguration() {
@Configuration
class A<T> {
int i;
}
var serializer = (ConfigurationSerializer<?>) SELECTOR.select(A.class);
assertThat(serializer.getConfigurationType(), equalTo(A.class));
}
@Test
void selectSerializerMissingType() {
assertThrowsConfigurationException(
() -> SELECTOR.select(Object.class),
"Missing serializer for type class java.lang.Object.\nEither annotate the type with " +
"@Configuration or provide a custom serializer by adding it to the properties."
);
}
@Test
void selectSerializerCustomType() {
var properties = ConfigurationProperties.newBuilder()
.addSerializer(Point.class, POINT_SERIALIZER)
.build();
SerializerSelector selector = new SerializerSelector(properties);
var pointSerializer = selector.select(Point.class);
assertThat(pointSerializer, sameInstance(POINT_SERIALIZER));
}
@Test
void selectSerializerCustomSerializerTakesPrecedence() {
var properties = ConfigurationProperties.newBuilder()
.addSerializer(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 selectSerializerList() {
class A {
List<String> ls;
}
var serializer = (ListSerializer<?, ?>) SELECTOR.select(getGenericType(A.class, "ls"));
assertThat(serializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerListNested() {
class A {
List<List<String>> lls;
}
var serializer = (ListSerializer<?, ?>) SELECTOR.select(getGenericType(A.class, "lls"));
var elementSerializer = (ListSerializer<?, ?>) serializer.getElementSerializer();
assertThat(elementSerializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerSetsAsSets() {
class A {
Set<String> ss;
}
SerializerSelector selector = new SerializerSelector(
ConfigurationProperties.newBuilder().serializeSetsAsLists(false).build()
);
var serializer = (SetSerializer<?, ?>) selector.select(getGenericType(A.class, "ss"));
assertThat(serializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerSetsAsLists() {
class A {
Set<String> ss;
}
var serializer = (SetAsListSerializer<?, ?>) SELECTOR.select(getGenericType(A.class, "ss"));
assertThat(serializer.getElementSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerMap() {
class A {
Map<Integer, String> mis;
}
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(getGenericType(A.class, "mis"));
var numberSerializer = (NumberSerializer) serializer.getKeySerializer();
assertThat(numberSerializer.getNumberClass(), equalTo(Integer.class));
assertThat(serializer.getValueSerializer(), instanceOf(StringSerializer.class));
}
@Test
void selectSerializerMapNested() {
enum E {}
class A {
Map<E, Set<List<E>>> mesle;
}
var serializer = (MapSerializer<?, ?, ?, ?>) SELECTOR.select(getGenericType(A.class, "mesle"));
var keySerializer = (EnumSerializer) serializer.getKeySerializer();
assertThat(keySerializer.getEnumCls(), equalTo(E.class));
var valSerializer = (SetAsListSerializer<?, ?>) serializer.getValueSerializer();
var listSerializer = (ListSerializer<?, ?>) valSerializer.getElementSerializer();
var enumSerializer = (EnumSerializer) listSerializer.getElementSerializer();
assertThat(enumSerializer.getEnumCls(), equalTo(E.class));
}
@Test
void selectSerializerMapInvalidKeyType1() {
class A {
Map<List<String>, String> mlss;
}
Type type = getGenericType(A.class, "mlss");
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type '" + type + "'.\nMap keys can only be " +
"of simple or enum type."
);
}
@Test
void selectSerializerMapInvalidKeyType2() {
class A {
Map<Point, String> mps;
}
Type type = getGenericType(A.class, "mps");
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type '" + type + "'.\nMap keys can only be " +
"of simple or enum type."
);
}
@Test
void selectSerializerOtherParameterizedType() {
class Box<T> {}
class A {
Box<String> box;
}
Type type = getGenericType(A.class, "box");
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type '" + type + "'.\nParameterized " +
"types other than lists, sets, and maps cannot be serialized."
);
}
@Test
void selectSerializerGenericArrayType() {
class A {
List<?>[] ga;
}
Type type = getGenericType(A.class, "ga");
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type 'java.util.List<?>[]'.\n" +
"Generic array types cannot be serialized."
);
}
@Test
void selectSerializerBoundedWildcardType() {
class A {
List<? extends String> les;
}
ParameterizedType ptype = (ParameterizedType) getGenericType(A.class, "les");
Type type = ptype.getActualTypeArguments()[0];
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type '? extends java.lang.String'.\n" +
"Wildcard types cannot be serialized."
);
}
@Test
void selectSerializerWildcardType() {
class A {
List<?> lw;
}
ParameterizedType ptype = (ParameterizedType) getGenericType(A.class, "lw");
Type type = ptype.getActualTypeArguments()[0];
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type '?'.\n" +
"Wildcard types cannot be serialized."
);
}
@Test
void selectSerializerTypeVariable() {
class A<T> {
T t;
}
Type type = getGenericType(A.class, "t");
assertThrowsConfigurationException(
() -> SELECTOR.select(type),
"Cannot select serializer for type 'T'.\n" +
"Type variables cannot be serialized."
);
}
}

@ -0,0 +1,908 @@
package de.exlll.configlib;
import de.exlll.configlib.Serializers.MapSerializer;
import de.exlll.configlib.Serializers.NumberSerializer;
import de.exlll.configlib.Serializers.SetAsListSerializer;
import de.exlll.configlib.Serializers.SetSerializer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static de.exlll.configlib.TestUtils.*;
import static java.util.Arrays.asList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
class SerializersTest {
@Test
void booleanSerializer() {
Serializer<Boolean, Boolean> serializer = new Serializers.BooleanSerializer();
Boolean element = true;
Object serialized = serializer.serialize(element);
assertThat(serialized, sameInstance(element));
Object deserialized = serializer.deserialize(element);
assertThat(deserialized, sameInstance(element));
}
@Test
void numberSerializerRequiresValidNumberType() {
assertThrowsNullPointerException(
() -> new NumberSerializer(null),
"number class"
);
assertThrowsIllegalArgumentException(
() -> new NumberSerializer(BigInteger.class),
"Class BigInteger is not a byte, short, int, long, float, double, or a " +
"wrapper type of one of the primitive number types."
);
}
@Test
void numberSerializerDeserializeInvalidType() {
NumberSerializer serializer = new NumberSerializer(int.class);
assertThrowsConfigurationException(
() -> serializer.deserialize(BigInteger.ONE),
"Cannot deserialize element '1' of type BigInteger.\n" +
"This serializer only supports primitive number types and their wrapper types."
);
}
@ParameterizedTest
@ValueSource(classes = {
byte.class, Byte.class,
short.class, Short.class,
int.class, Integer.class,
long.class, Long.class
})
void numberSerializerSerializeIntegerType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
int[] numbers = {Integer.MIN_VALUE, 0, Integer.MAX_VALUE};
for (int number : numbers) {
Number serialized = serializer.serialize(number);
assertThat(serialized, instanceOf(Long.class));
assertThat(serialized, is((long) number));
}
}
@ParameterizedTest
@ValueSource(classes = {
float.class, Float.class,
double.class, Double.class
})
void numberSerializerSerializeFloatingPointType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
float[] numbers = {-Float.MAX_VALUE, -Float.MIN_VALUE, 0, Float.MIN_VALUE, Float.MAX_VALUE};
for (float number : numbers) {
Number serialized = serializer.serialize(number);
assertThat(serialized, instanceOf(Double.class));
assertThat(serialized, is((double) number));
}
}
@ParameterizedTest
@ValueSource(classes = {
byte.class, Byte.class,
short.class, Short.class,
int.class, Integer.class,
long.class, Long.class
})
void numberSerializerDeserializeByteToIntegerType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
byte[] numbers = {Byte.MIN_VALUE, 0, Byte.MAX_VALUE};
for (byte number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized.longValue(), is((long) number));
}
}
@ParameterizedTest
@ValueSource(classes = {
short.class, Short.class,
int.class, Integer.class,
long.class, Long.class
})
void numberSerializerDeserializeShortToIntegerType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
short[] numbers = {Short.MIN_VALUE, 0, Short.MAX_VALUE};
for (short number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized.longValue(), is((long) number));
}
}
@ParameterizedTest
@ValueSource(classes = {int.class, Integer.class, long.class, Long.class})
void numberSerializerDeserializeIntegerToIntegerType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
int[] numbers = {Integer.MIN_VALUE, 0, Integer.MAX_VALUE};
for (int number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized.longValue(), is((long) number));
}
}
@ParameterizedTest
@ValueSource(classes = {long.class, Long.class})
void numberSerializerDeserializeLongToIntegerType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
long[] numbers = {Long.MIN_VALUE, 0, Long.MAX_VALUE};
for (long number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized, is(number));
}
}
@ParameterizedTest
@ValueSource(classes = {byte.class, Byte.class})
void numberSerializerDeserializeTooLargeIntegerTypeToByte(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
long[] values = {
Long.MIN_VALUE, Integer.MIN_VALUE, Short.MIN_VALUE, Byte.MIN_VALUE - 1,
Long.MAX_VALUE, Integer.MAX_VALUE, Short.MAX_VALUE, Byte.MAX_VALUE + 1
};
for (long value : values) {
String msg = "Number " + value + " cannot be converted to type " + cls.getSimpleName() +
". It does not fit into the range of valid values [-128, 127].";
assertThrowsConfigurationException(() -> serializer.deserialize(value), msg);
}
}
@ParameterizedTest
@ValueSource(classes = {short.class, Short.class})
void numberSerializerDeserializeTooLargeIntegerTypeToShort(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
long[] values = {
Long.MIN_VALUE, Integer.MIN_VALUE, Short.MIN_VALUE - 1,
Long.MAX_VALUE, Integer.MAX_VALUE, Short.MAX_VALUE + 1,
};
for (long value : values) {
String msg = "Number " + value + " cannot be converted to type " + cls.getSimpleName() +
". It does not fit into the range of valid values [-32768, 32767].";
assertThrowsConfigurationException(() -> serializer.deserialize(value), msg);
}
}
@ParameterizedTest
@ValueSource(classes = {int.class, Integer.class})
void numberSerializerDeserializeTooLargeIntegerTypeToInteger(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
long[] values = {
Long.MIN_VALUE, Integer.MIN_VALUE - 1L,
Long.MAX_VALUE, Integer.MAX_VALUE + 1L,
};
for (long value : values) {
String msg = "Number " + value + " cannot be converted to type " + cls.getSimpleName() +
". It does not fit into the range of valid values [-2147483648, 2147483647].";
assertThrowsConfigurationException(() -> serializer.deserialize(value), msg);
}
}
@ParameterizedTest
@ValueSource(classes = {float.class, Float.class, double.class, Double.class})
void numberSerializerDeserializeFloatToFloatingPointType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
float[] numbers = {-Float.MAX_VALUE, -Float.MIN_VALUE, 0, Float.MIN_VALUE, Float.MAX_VALUE};
for (float number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized.doubleValue(), is((double) number));
}
}
@ParameterizedTest
@ValueSource(classes = {double.class, Double.class})
void numberSerializerDeserializeDoubleToFloatingPointType(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
double[] numbers = {-Double.MAX_VALUE, -Double.MIN_VALUE, 0, Double.MIN_VALUE, Double.MAX_VALUE};
for (double number : numbers) {
Number deserialized = serializer.deserialize(number);
assertThat(deserialized, instanceOf(cls));
assertThat(deserialized, is(number));
}
}
@ParameterizedTest
@ValueSource(classes = {float.class, Float.class})
void numberSerializerDeserializeTooLargeFloatingPointTypeToFloat(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
double ulpFloatMax = Math.ulp((double) Float.MAX_VALUE);
double[] values = {
+Double.MAX_VALUE,
-Double.MAX_VALUE,
(double) +Float.MAX_VALUE + ulpFloatMax,
(double) -Float.MAX_VALUE - ulpFloatMax
};
for (double value : values) {
String clsName = cls.getSimpleName();
String msg = "Number " + value + " cannot be converted to type " + clsName + ". " +
"It is larger than the largest possible " + clsName + " value.";
assertThrowsConfigurationException(() -> serializer.deserialize(value), msg);
}
}
@ParameterizedTest
@ValueSource(classes = {float.class, Float.class})
void numberSerializerDeserializeTooSmallFloatingPointTypeToFloat(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
double ulpFloatMin = Math.ulp((double) Float.MIN_VALUE);
double[] values = {
+Double.MIN_VALUE,
-Double.MIN_VALUE,
(double) +Float.MIN_VALUE - ulpFloatMin,
(double) -Float.MIN_VALUE + ulpFloatMin,
};
for (double value : values) {
String clsName = cls.getSimpleName();
String msg = "Number " + value + " cannot be converted to type " + clsName + ". " +
"It is smaller than the smallest possible " + clsName + " value.";
assertThrowsConfigurationException(() -> serializer.deserialize(value), msg);
}
}
@ParameterizedTest
@ValueSource(classes = {float.class, Float.class})
void numberSerializerDeserializeFloatSpecialValues(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
numberSerializerDeserializeFloatingPointSpecialValues(
serializer,
Float.class,
Float.POSITIVE_INFINITY,
Float.NEGATIVE_INFINITY,
Float.NaN
);
}
@ParameterizedTest
@ValueSource(classes = {double.class, Double.class})
void numberSerializerDeserializeDoubleSpecialValues(Class<? extends Number> cls) {
NumberSerializer serializer = new NumberSerializer(cls);
numberSerializerDeserializeFloatingPointSpecialValues(
serializer,
Double.class,
Double.POSITIVE_INFINITY,
Double.NEGATIVE_INFINITY,
Double.NaN
);
}
private <T> void numberSerializerDeserializeFloatingPointSpecialValues(
NumberSerializer serializer,
Class<T> type,
T positiveInfinity,
T negativeInfinity,
T nan
) {
Number pinfDouble = serializer.deserialize(Double.POSITIVE_INFINITY);
Number ninfDouble = serializer.deserialize(Double.NEGATIVE_INFINITY);
Number nanDouble = serializer.deserialize(Double.NaN);
Number pinfFloat = serializer.deserialize(Float.POSITIVE_INFINITY);
Number ninfFloat = serializer.deserialize(Float.NEGATIVE_INFINITY);
Number nanFloat = serializer.deserialize(Float.NaN);
assertThat(pinfDouble, instanceOf(type));
assertThat(ninfDouble, instanceOf(type));
assertThat(nanDouble, instanceOf(type));
assertThat(pinfFloat, instanceOf(type));
assertThat(ninfFloat, instanceOf(type));
assertThat(nanFloat, instanceOf(type));
assertThat(pinfDouble, is(positiveInfinity));
assertThat(ninfDouble, is(negativeInfinity));
assertThat(nanDouble, is(nan));
assertThat(pinfFloat, is(positiveInfinity));
assertThat(ninfFloat, is(negativeInfinity));
assertThat(nanFloat, is(nan));
}
@Test
void stringSerializer() {
Serializer<String, String> serializer = new Serializers.StringSerializer();
String random = "RANDOM";
assertThat(serializer.serialize(random), sameInstance(random));
assertThat(serializer.deserialize(random), sameInstance(random));
}
@Test
void characterSerializer() {
Serializer<Character, String> serializer = new Serializers.CharacterSerializer();
assertThat(serializer.serialize('c'), is("c"));
assertThat(serializer.deserialize("c"), is('c'));
}
@Test
void characterSerializerDeserializeInvalidString() {
Serializer<Character, String> serializer = new Serializers.CharacterSerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(""),
"An empty string cannot be converted to a character."
);
assertThrowsConfigurationException(
() -> serializer.deserialize("ab"),
"String 'ab' is too long to be converted to a character."
);
}
@Test
void bigIntegerSerializer() {
Serializer<BigInteger, String> serializer = new Serializers.BigIntegerSerializer();
final String result = "18446744073709551614";
BigInteger integer = BigInteger.valueOf(Long.MAX_VALUE).multiply(BigInteger.TWO);
assertThat(serializer.serialize(integer), is(result));
integer = new BigInteger(result);
assertThat(serializer.deserialize(result), is(integer));
}
@Test
void bigDecimalSerializer() {
Serializer<BigDecimal, String> serializer = new Serializers.BigDecimalSerializer();
final String result = "3.23170060713109998320439596646649E+616";
final BigDecimal doubleMax = BigDecimal.valueOf(Double.MAX_VALUE);
BigDecimal decimal = doubleMax.multiply(doubleMax);
assertThat(serializer.serialize(decimal), is(result));
decimal = new BigDecimal(result);
assertThat(serializer.deserialize(result), is(decimal));
}
@Test
void localDateSerializer() {
Serializer<LocalDate, String> serializer = new Serializers.LocalDateSerializer();
LocalDate date = LocalDate.of(2000, Month.FEBRUARY, 29);
assertThat(serializer.serialize(date), is("2000-02-29"));
assertThat(serializer.deserialize("2000-02-29"), is(date));
}
@Test
void localTimeSerializer() {
Serializer<LocalTime, String> serializer = new Serializers.LocalTimeSerializer();
LocalTime time = LocalTime.of(10, 11, 12, 13);
assertThat(serializer.serialize(time), is("10:11:12"));
time = time.truncatedTo(ChronoUnit.SECONDS);
assertThat(serializer.deserialize("10:11:12"), is(time));
}
@Test
void localDateTimeSerializer() {
Serializer<LocalDateTime, String> serializer = new Serializers.LocalDateTimeSerializer();
LocalDateTime dateTime = LocalDateTime.of(2000, Month.FEBRUARY, 29, 10, 11, 12, 13);
assertThat(serializer.serialize(dateTime), is("2000-02-29T10:11:12"));
dateTime = dateTime.truncatedTo(ChronoUnit.SECONDS);
assertThat(serializer.deserialize("2000-02-29T10:11:12"), is(dateTime));
}
enum E {X, Y, Z}
@ParameterizedTest
@EnumSource(E.class)
void enumSerializer(Enum<?> e) {
Serializers.EnumSerializer serializer = new Serializers.EnumSerializer(E.class);
assertThat(serializer.serialize(e), is(e.name()));
assertThat(serializer.deserialize(e.name()), is(e));
}
@Test
void enumSerializerMissingValue() {
Serializers.EnumSerializer serializer = new Serializers.EnumSerializer(E.class);
assertThrowsConfigurationException(
() -> serializer.deserialize("A"),
"Enum class E does not contain enum 'A'. Valid values are: [X, Y, Z]"
);
}
@Test
void listSerializer() {
var serializer = new Serializers.ListSerializer<>(StringToIntSerializer.INSTANCE, false, false);
List<Integer> integers = serializer.serialize(List.of("1", "2", "3"));
assertThat(integers, is(List.of(1, 2, 3)));
List<String> strings = serializer.deserialize(List.of(4, 5, 6));
assertThat(strings, is(List.of("4", "5", "6")));
}
@Test
void listSerializerNoNulls() {
var serializer = new Serializers.ListSerializer<>(StringToIntSerializer.INSTANCE, false, false);
List<Integer> integers = serializer.serialize(asList("1", null, "2", null, "3"));
assertThat(integers, is(List.of(1, 2, 3)));
List<String> strings = serializer.deserialize(asList(4, null, 5, null, 6));
assertThat(strings, is(List.of("4", "5", "6")));
}
@Test
void listSerializerOutputNulls() {
var serializer = new Serializers.ListSerializer<>(StringToIntSerializer.INSTANCE, true, false);
List<Integer> actual = serializer.serialize(asList("1", null, "3"));
assertThat(actual, is(asList(1, null, 3)));
}
@Test
void listSerializerInputNulls() {
var serializer = new Serializers.ListSerializer<>(StringToIntSerializer.INSTANCE, false, true);
List<String> actual = serializer.deserialize(asList(4, null, 5));
assertThat(actual, is(asList("4", null, "5")));
}
@Test
void setSerializer() {
var serializer = new SetSerializer<>(StringToIntSerializer.INSTANCE, false, false);
Set<Integer> integers = serializer.serialize(Set.of("1", "2", "3"));
assertThat(integers, is(Set.of(1, 2, 3)));
Set<String> strings = serializer.deserialize(Set.of(4, 5, 6));
assertThat(strings, is(Set.of("4", "5", "6")));
}
@Test
void setSerializerNoNulls() {
var serializer = new SetSerializer<>(StringToIntSerializer.INSTANCE, false, false);
Set<Integer> integers = serializer.serialize(asSet("1", null, "2", null, "3"));
assertThat(integers, is(asSet(1, 2, 3)));
Set<String> strings = serializer.deserialize(asSet(4, null, 5, null, 6));
assertThat(strings, is(asSet("4", "5", "6")));
}
@Test
void setSerializerOutputNulls() {
var serializer = new SetSerializer<>(StringToIntSerializer.INSTANCE, true, false);
Set<Integer> actual = serializer.serialize(asSet("1", null, "3"));
assertThat(actual, is(asSet(1, null, 3)));
}
@Test
void setSerializerInputNulls() {
var serializer = new SetSerializer<>(StringToIntSerializer.INSTANCE, false, true);
Set<String> actual = serializer.deserialize(asSet(4, null, 5));
assertThat(actual, is(asSet("4", null, "5")));
}
@Test
void setAsListSerializer() {
var serializer = new SetAsListSerializer<>(StringToIntSerializer.INSTANCE, false, false);
List<Integer> integers = serializer.serialize(asSet("1", "2", "3"));
assertThat(integers, is(asList(1, 2, 3)));
Set<String> strings = serializer.deserialize(List.of(4, 5, 6));
assertThat(strings, is(Set.of("4", "5", "6")));
}
@Test
void setAsListSerializerNoNulls() {
var serializer = new SetAsListSerializer<>(StringToIntSerializer.INSTANCE, false, false);
List<Integer> integers = serializer.serialize(asSet("1", null, "2", null, "3"));
assertThat(integers, is(asList(1, 2, 3)));
Set<String> strings = serializer.deserialize(asList(4, null, 5, null, 6));
assertThat(strings, is(asSet("4", "5", "6")));
}
@Test
void setAsListSerializerOutputNulls() {
var serializer = new SetAsListSerializer<>(StringToIntSerializer.INSTANCE, true, false);
List<Integer> actual = serializer.serialize(asSet("1", null, "3"));
assertThat(actual, is(asList(1, null, 3)));
}
@Test
void setAsListSerializerInputNulls() {
var serializer = new SetAsListSerializer<>(StringToIntSerializer.INSTANCE, false, true);
Set<String> actual = serializer.deserialize(asList(4, null, 5));
assertThat(actual, is(asSet("4", null, "5")));
}
@Test
void mapSerializer() {
var serializer = new MapSerializer<>(
StringToIntSerializer.INSTANCE,
StringToIntSerializer.INSTANCE,
false,
false
);
Map<Integer, Integer> integers = serializer.serialize(Map.of("1", "2", "3", "4"));
assertThat(integers, is(Map.of(1, 2, 3, 4)));
Map<String, String> strings = serializer.deserialize(Map.of(5, 6, 7, 8));
assertThat(strings, is(Map.of("5", "6", "7", "8")));
}
@Test
void mapSerializerNoNulls() {
var serializer = new MapSerializer<>(
StringToIntSerializer.INSTANCE,
StringToIntSerializer.INSTANCE,
false,
false
);
Map<Integer, Integer> integers = serializer.serialize(entriesAsMap(
entry("1", "2"),
entry(null, "4"),
entry("5", null),
entry(null, null),
entry("9", "10")
));
assertThat(integers, is(Map.of(1, 2, 9, 10)));
Map<String, String> strings = serializer.deserialize(entriesAsMap(
entry(11, 12),
entry(null, 14),
entry(15, null),
entry(null, null),
entry(19, 20)
));
assertThat(strings, is(Map.of("11", "12", "19", "20")));
}
@Test
void mapSerializerOutputNulls() {
var serializer = new MapSerializer<>(
StringToIntSerializer.INSTANCE,
StringToIntSerializer.INSTANCE,
true,
false
);
Map<Integer, Integer> actual = serializer.serialize(entriesAsMap(
entry("1", "2"),
entry(null, "4"),
entry("5", null),
entry(null, null),
entry("9", "10")
));
Map<Integer, Integer> expected = entriesAsMap(
entry(1, 2),
entry(5, null),
entry(null, null),
entry(9, 10)
);
assertThat(actual, is(expected));
}
@Test
void mapSerializerInputNulls() {
var serializer = new MapSerializer<>(
StringToIntSerializer.INSTANCE,
StringToIntSerializer.INSTANCE,
false,
true
);
Map<String, String> actual = serializer.deserialize(entriesAsMap(
entry(11, 12),
entry(null, 14),
entry(15, null),
entry(null, null),
entry(19, 20)
));
Map<String, String> expected = entriesAsMap(
entry("11", "12"),
entry("15", null),
entry(null, null),
entry("19", "20")
);
assertThat(actual, is(expected));
}
@Test
void arraySerializer() {
var serializer = new Serializers.ArraySerializer<>(
String.class,
StringToIntSerializer.INSTANCE,
false, false
);
List<Integer> integers = serializer.serialize(new String[]{"1", "2", "3"});
assertThat(integers, is(List.of(1, 2, 3)));
String[] strings = serializer.deserialize(List.of(4, 5, 6));
assertThat(strings, is(new String[]{"4", "5", "6"}));
}
@Test
void arraySerializerNested() {
var serializer = new Serializers.ArraySerializer<>(
String[].class,
new Serializers.ArraySerializer<>(
String.class,
StringToIntSerializer.INSTANCE,
false, false
),
false, false
);
String[][] input = {{"1"}, {"2", "3"}};
List<List<Integer>> integers = serializer.serialize(input);
assertThat(integers, is(List.of(List.of(1), List.of(2, 3))));
String[][] output = serializer.deserialize(List.of(
List.of(4),
List.of(5, 6)
));
assertThat(output, is(new String[][]{{"4"}, {"5", "6"}}));
}
@Test
void arraySerializerNoNulls() {
var serializer = new Serializers.ArraySerializer<>(
String.class,
StringToIntSerializer.INSTANCE,
false, false
);
List<Integer> integers = serializer.serialize(new String[]{"1", null, "2", null, "3"});
assertThat(integers, is(List.of(1, 2, 3)));
String[] strings = serializer.deserialize(asList(4, null, 5, null, 6));
assertThat(strings, is(new String[]{"4", "5", "6"}));
}
@Test
void arraySerializerOutputNulls() {
var serializer = new Serializers.ArraySerializer<>(
String.class,
StringToIntSerializer.INSTANCE,
true, false
);
List<Integer> integers = serializer.serialize(new String[]{"1", null, "2", null, "3"});
assertThat(integers, is(asList(1, null, 2, null, 3)));
}
@Test
void arraySerializerInputNulls() {
var serializer = new Serializers.ArraySerializer<>(
String.class,
StringToIntSerializer.INSTANCE,
false, true
);
String[] strings = serializer.deserialize(asList(4, null, 5, null, 6));
assertThat(strings, is(new String[]{"4", null, "5", null, "6"}));
}
@Test
void primitiveBooleanArraySerializer() {
var serializer = new Serializers.PrimitiveBooleanArraySerializer();
List<Boolean> list = serializer.serialize(new boolean[]{true, false, true});
assertThat(list, is(List.of(true, false, true)));
boolean[] array = (boolean[]) serializer.deserialize(List.of(false, true, false));
assertThat(array, is(new boolean[]{false, true, false}));
}
@Test
void primitiveCharacterArraySerializer() {
var serializer = new Serializers.PrimitiveCharacterArraySerializer();
List<String> list = serializer.serialize(new char[]{'a', 'b', 'c'});
assertThat(list, is(List.of("a", "b", "c")));
char[] array = (char[]) serializer.deserialize(List.of("d", "e", "f"));
assertThat(array, is(new char[]{'d', 'e', 'f'}));
}
@Test
void primitiveByteArraySerializer() {
var serializer = new Serializers.PrimitiveByteArraySerializer();
List<Number> list = serializer.serialize(new byte[]{(byte) 1, (byte) 2, (byte) 3});
assertThat(list, is(List.of(1L, 2L, 3L)));
byte[] array = (byte[]) serializer.deserialize(List.of(3L, 4L, 5L));
assertThat(array, is(new byte[]{(byte) 3, (byte) 4, (byte) 5}));
}
@Test
void primitiveShortArraySerializer() {
var serializer = new Serializers.PrimitiveShortArraySerializer();
List<Number> list = serializer.serialize(new short[]{(short) 1, (short) 2, (short) 3});
assertThat(list, is(List.of(1L, 2L, 3L)));
short[] array = (short[]) serializer.deserialize(List.of(3L, 4L, 5L));
assertThat(array, is(new short[]{(short) 3, (short) 4, (short) 5}));
}
@Test
void primitiveIntegerArraySerializer() {
var serializer = new Serializers.PrimitiveIntegerArraySerializer();
List<Number> list = serializer.serialize(new int[]{1, 2, 3});
assertThat(list, is(List.of(1L, 2L, 3L)));
int[] array = (int[]) serializer.deserialize(List.of(3L, 4L, 5L));
assertThat(array, is(new int[]{3, 4, 5}));
}
@Test
void primitiveLongArraySerializer() {
var serializer = new Serializers.PrimitiveLongArraySerializer();
List<Number> list = serializer.serialize(new long[]{1L, 2L, 3L});
assertThat(list, is(List.of(1L, 2L, 3L)));
long[] array = (long[]) serializer.deserialize(List.of(3L, 4L, 5L));
assertThat(array, is(new long[]{3L, 4L, 5L}));
}
@Test
void primitiveFloatArraySerializer() {
var serializer = new Serializers.PrimitiveFloatArraySerializer();
List<Number> list = serializer.serialize(new float[]{1f, 2f, 3f});
assertThat(list, is(List.of(1d, 2d, 3d)));
float[] array = (float[]) serializer.deserialize(List.of(3d, 4d, 5d));
assertThat(array, is(new float[]{3f, 4f, 5f}));
}
@Test
void primitiveDoubleArraySerializer() {
var serializer = new Serializers.PrimitiveDoubleArraySerializer();
List<Number> list = serializer.serialize(new double[]{1d, 2d, 3d});
assertThat(list, is(List.of(1d, 2d, 3d)));
double[] array = (double[]) serializer.deserialize(List.of(3d, 4d, 5d));
assertThat(array, is(new double[]{3d, 4d, 5d}));
}
@Test
void primitiveBooleanArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveBooleanArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList(true, false, null)),
"The boolean element at index 2 must not be null."
);
}
@Test
void primitiveCharacterArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveCharacterArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList("a", null, "b")),
"The char element at index 1 must not be null."
);
}
@Test
void primitiveByteArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveByteArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList((byte) 1, (byte) 2, null)),
"The byte element at index 2 must not be null."
);
}
@Test
void primitiveShortArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveShortArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList((short) 1, null, (short) 2)),
"The short element at index 1 must not be null."
);
}
@Test
void primitiveIntegerArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveIntegerArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList(1, 2, null)),
"The int element at index 2 must not be null."
);
}
@Test
void primitiveLongArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveLongArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList(1L, null, 2L)),
"The long element at index 1 must not be null."
);
}
@Test
void primitiveFloatArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveFloatArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList(1f, 2f, null)),
"The float element at index 2 must not be null."
);
}
@Test
void primitiveDoubleArraySerializerDeserializeNullElement() {
var serializer = new Serializers.PrimitiveDoubleArraySerializer();
assertThrowsConfigurationException(
() -> serializer.deserialize(asList(1d, null, 2d)),
"The double element at index 1 must not be null."
);
}
private static final class StringToIntSerializer implements Serializer<String, Integer> {
private static final StringToIntSerializer INSTANCE = new StringToIntSerializer();
@Override
public Integer serialize(String element) {
return Integer.valueOf(element);
}
@Override
public String deserialize(Integer element) {
return element.toString();
}
}
}

@ -0,0 +1,229 @@
package de.exlll.configlib;
import org.junit.jupiter.api.function.Executable;
import java.awt.Point;
import java.io.IOException;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public final class TestUtils {
public static final PointSerializer POINT_SERIALIZER = new PointSerializer();
public static final PointIdentitySerializer POINT_IDENTITY_SERIALIZER =
new PointIdentitySerializer();
public static final CustomBigIntegerSerializer CUSTOM_BIG_INTEGER_SERIALIZER
= new CustomBigIntegerSerializer();
public static Field getField(Class<?> cls, String fieldName) {
return Arrays.stream(cls.getDeclaredFields())
.filter(field -> field.getName().equals(fieldName))
.findAny()
.orElseThrow();
}
public static void assertThrowsNullPointerException(Executable executable, String argumentName) {
String msg = "The " + argumentName + " must not be null.";
assertThrowsException(NullPointerException.class, executable, msg);
}
public static void assertThrowsIllegalArgumentException(
Executable executable,
String expectedExceptionMessage
) {
assertThrowsException(IllegalArgumentException.class, executable, expectedExceptionMessage);
}
public static void assertThrowsConfigurationException(
Executable executable,
String expectedExceptionMessage
) {
assertThrowsException(ConfigurationException.class, executable, expectedExceptionMessage);
}
public static void assertThrowsRuntimeException(
Executable executable,
String expectedExceptionMessage
) {
assertThrowsException(RuntimeException.class, executable, expectedExceptionMessage);
}
public static <T extends Exception> void assertThrowsException(
Class<T> exceptionType,
Executable executable,
String expectedExceptionMessage
) {
T exception = assertThrows(exceptionType, executable);
assertEquals(expectedExceptionMessage, exception.getMessage());
}
public static final class CustomBigIntegerSerializer implements Serializer<BigInteger, String> {
@Override
public String serialize(BigInteger element) {
return element.multiply(BigInteger.TWO).toString();
}
@Override
public BigInteger deserialize(String element) {
return new BigInteger(element).divide(BigInteger.TWO);
}
}
public static final class PointSerializer implements Serializer<Point, String> {
@Override
public String serialize(Point element) {
return element.x + ":" + element.y;
}
@Override
public Point deserialize(String element) {
String[] parts = element.split(":");
int x = Integer.parseInt(parts[0]);
int y = Integer.parseInt(parts[1]);
return new Point(x, y);
}
}
public static final class PointIdentitySerializer implements Serializer<Point, Point> {
@Override
public Point serialize(Point element) {
return element;
}
@Override
public Point deserialize(Point element) {
return element;
}
}
@SafeVarargs
public static <E> Set<E> asSet(E... elements) {
return new LinkedHashSet<>(Arrays.asList(elements));
}
public static <K, V> Map<K, V> asMap() {
return new LinkedHashMap<>();
}
public static <K, V> Map<K, V> asMap(K k1, V v1) {
final Map<K, V> result = new LinkedHashMap<>();
result.put(k1, v1);
return result;
}
public static <K, V> Map<K, V> asMap(K k1, V v1, K k2, V v2) {
final Map<K, V> result = new LinkedHashMap<>();
result.put(k1, v1);
result.put(k2, v2);
return result;
}
public static <K, V> Map<K, V> asMap(K k1, V v1, K k2, V v2, K k3, V v3) {
final Map<K, V> result = new LinkedHashMap<>();
result.put(k1, v1);
result.put(k2, v2);
result.put(k3, v3);
return result;
}
public static <K, V> Map<K, V> asMap(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {
final Map<K, V> result = new LinkedHashMap<>();
result.put(k1, v1);
result.put(k2, v2);
result.put(k3, v3);
result.put(k4, v4);
return result;
}
public static <K, V> Map<K, V> entriesAsMap(MEntry... entries) {
final Map<Object, Object> result = new LinkedHashMap<>();
Arrays.stream(entries).forEach(entry -> result.put(entry.getKey(), entry.getValue()));
// Suppressing this warning might lead to an exception.
// Using proper generics for the MEntry class is possible.
// However, doing so increases the compilation type by several seconds
@SuppressWarnings("unchecked")
Map<K, V> returnResult = (Map<K, V>) result;
return returnResult;
}
public static <K, V> Map<K, V> extend(Map<K, V> base, MEntry... entries) {
final Map<K, V> result = new LinkedHashMap<>(base);
final Map<K, V> ext = entriesAsMap(entries);
result.putAll(ext);
return result;
}
public static MEntry entry(Object key, Object val) {
return new MEntry(key, val);
}
public static final class MEntry implements Map.Entry<Object, Object> {
private final Object key;
private final Object val;
public MEntry(Object key, Object val) {
this.key = key;
this.val = val;
}
@Override
public Object getKey() {
return key;
}
@Override
public Object getValue() {
return val;
}
@Override
public Object setValue(Object value) {
throw new UnsupportedOperationException("setValue not supported");
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MEntry mEntry = (MEntry) o;
return Objects.equals(key, mEntry.key) && Objects.equals(val, mEntry.val);
}
@Override
public int hashCode() {
return Objects.hash(key, val);
}
}
public static <T, C extends Collection<T[]>> boolean collectionOfArraysDeepEquals(
C collection1,
C collection2,
Supplier<Collection<List<T>>> collectionFactory
) {
Collection<List<T>> c1 = collection1.stream().map(Arrays::asList)
.collect(Collectors.toCollection(collectionFactory));
Collection<List<T>> c2 = collection2.stream().map(Arrays::asList)
.collect(Collectors.toCollection(collectionFactory));
return c1.equals(c2);
}
public static String readFile(Path file) {
try (Stream<String> lines = Files.lines(file)) {
return lines.collect(joining("\n"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,235 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.awt.Point;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static de.exlll.configlib.TestUtils.*;
import static org.junit.jupiter.api.Assertions.*;
class YamlConfigurationStoreTest {
private final FileSystem fs = Jimfs.newFileSystem();
private final Path yamlFile = fs.getPath("/tmp/config.yml");
@BeforeEach
void setUp() throws IOException {
Files.createDirectories(yamlFile.getParent());
}
@AfterEach
void tearDown() throws IOException {
fs.close();
}
@Configuration
static final class A {
String s = "S1";
@Comment("A comment")
Integer i = null;
}
@Test
void save() {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.header("The\nHeader")
.footer("The\nFooter")
.outputNulls(true)
.setFieldFormatter(field -> field.getName().toUpperCase())
.build();
YamlConfigurationStore<A> store = new YamlConfigurationStore<>(A.class, properties);
store.save(new A(), yamlFile);
String expected =
"""
# The
# Header
S: S1
# A comment
I: null
# The
# Footer\
""";
assertEquals(expected, TestUtils.readFile(yamlFile));
}
@Configuration
static final class B {
String s = "S1";
String t = "T1";
Integer i = 1;
}
@Test
void load() throws IOException {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.inputNulls(true)
.setFieldFormatter(field -> field.getName().toUpperCase())
.build();
YamlConfigurationStore<B> store = new YamlConfigurationStore<>(B.class, properties);
Files.writeString(
yamlFile,
"""
# The
# Header
S: S2
t: T2
I: null
# The
# Footer\
"""
);
B config = store.load(yamlFile);
assertEquals("S2", config.s);
assertEquals("T1", config.t);
assertNull(config.i);
}
@Configuration
static final class C {
int i;
}
@Test
void loadInvalidYaml() throws IOException {
YamlConfigurationStore<C> store = newDefaultStore(C.class);
Files.writeString(
yamlFile,
"""
- - - - - a
a
"""
);
assertThrowsConfigurationException(
() -> store.load(yamlFile),
"The configuration file at /tmp/config.yml does not contain valid YAML."
);
}
@Test
void loadEmptyYaml() throws IOException {
YamlConfigurationStore<C> store = newDefaultStore(C.class);
Files.writeString(yamlFile, "null");
assertThrowsConfigurationException(
() -> store.load(yamlFile),
"The configuration file at /tmp/config.yml is empty or only contains null."
);
}
@Test
void loadNonMapYaml() throws IOException {
YamlConfigurationStore<C> store = newDefaultStore(C.class);
Files.writeString(yamlFile, "a");
assertThrowsConfigurationException(
() -> store.load(yamlFile),
"The contents of the YAML file at /tmp/config.yml do not represent a " +
"configuration. A valid configuration file contains a YAML map but instead a " +
"'class java.lang.String' was found."
);
}
@Configuration
static final class D {
Point point = new Point(1, 2);
}
@Test
void saveConfigurationWithInvalidTargetType() {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.addSerializer(Point.class, TestUtils.POINT_IDENTITY_SERIALIZER)
.build();
YamlConfigurationStore<D> store = new YamlConfigurationStore<>(D.class, properties);
assertThrowsConfigurationException(
() -> store.save(new D(), yamlFile),
"The given configuration could not be converted into YAML. \n" +
"Do all custom serializers produce valid target types?"
);
}
@Test
void saveCreatesParentDirectoriesIfPropertyTrue() {
YamlConfigurationStore<A> store = newDefaultStore(A.class);
Path file = fs.getPath("/a/b/c.yml");
store.save(new A(), file);
assertTrue(Files.exists(file.getParent()));
assertTrue(Files.exists(file));
}
@Test
void saveDoesNotCreateParentDirectoriesIfPropertyFalse() {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
.createParentDirectories(false)
.build();
YamlConfigurationStore<A> store = new YamlConfigurationStore<>(A.class, properties);
Path file = fs.getPath("/a/b/c.yml");
assertThrowsRuntimeException(
() -> store.save(new A(), file),
"java.nio.file.NoSuchFileException: /a/b/c.yml"
);
}
@Configuration
static final class E {
int i = 10;
int j = 11;
}
@Test
void updateCreatesConfigurationFileIfItDoesNotExist() {
YamlConfigurationStore<E> store = newDefaultStore(E.class);
assertFalse(Files.exists(yamlFile));
E config = store.update(yamlFile);
assertEquals("i: 10\nj: 11", readFile(yamlFile));
assertEquals(10, config.i);
assertEquals(11, config.j);
}
@Test
void updateLoadsConfigurationFileIfItDoesExist() throws IOException {
YamlConfigurationStore<E> store = newDefaultStore(E.class);
Files.writeString(yamlFile, "i: 20");
E config = store.update(yamlFile);
assertEquals(20, config.i);
assertEquals(11, config.j);
}
@Test
void updateUpdatesFile() throws IOException {
YamlConfigurationStore<E> store = newDefaultStore(E.class);
Files.writeString(yamlFile, "i: 20\nk: 30");
E config = store.update(yamlFile);
assertEquals(20, config.i);
assertEquals(11, config.j);
assertEquals("i: 20\nj: 11", readFile(yamlFile));
}
private static <T> YamlConfigurationStore<T> newDefaultStore(Class<T> configType) {
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
return new YamlConfigurationStore<>(configType, properties);
}
}

@ -0,0 +1,418 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.YamlConfigurationStore.YamlFileWriter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.snakeyaml.engine.v2.api.Dump;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SuppressWarnings("unused")
class YamlFileWriterTest {
private final FileSystem fs = Jimfs.newFileSystem();
private final Path yamlFile = fs.getPath("/tmp/config.yml");
@BeforeEach
void setUp() throws IOException {
Files.createDirectories(yamlFile.getParent());
}
@AfterEach
void tearDown() throws IOException {
fs.close();
}
@Configuration
static final class A {
String s = "";
}
@Test
void writeYamlWithNoComments() {
writeConfig(A.class);
assertFileContentEquals("s: ''");
}
@Test
void writeYamlWithHeaderAndFooter() {
writeConfig(
A.class,
builder -> builder
.header("This is a \n\n \nheader.")
.footer("That is a\n\n \nfooter.")
);
assertFileContentEquals(
"""
# This is a\s
# \s
# header.
s: ''
# That is a
# \s
# footer.\
"""
);
}
@Configuration
static final class B {
@Comment("Hello")
String s = "s";
}
@Test
void writeYamlSingleComment() {
writeConfig(B.class);
assertFileContentEquals(
"""
# Hello
s: s\
"""
);
}
@Configuration
static final class C {
@Comment({"Hello", "World"})
Map<String, Integer> mapStringInteger = Map.of("1", 2);
@Comment({"world", "hello"})
Map<Integer, String> mapIntegerString = Map.of(2, "1");
}
@Test
void writeYamlMultipleComments() {
writeConfig(C.class);
assertFileContentEquals(
"""
# Hello
# World
mapStringInteger:
'1': 2
# world
# hello
mapIntegerString:
2: '1'\
"""
);
}
@Configuration
static final class D {
@Comment({"Hello", "", " ", "World"})
String s1 = "s1";
@Comment({"", "", " ", "How are ", "you?", ""})
String s2 = "s2";
}
@Test
void writeYamlEmptyComments() {
writeConfig(D.class);
assertFileContentEquals(
"""
# Hello
# \s
# World
s1: s1
# \s
# How are\s
# you?
s2: s2\
"""
);
}
@Configuration
static final class E1 {
@Comment("m")
Map<String, Map<String, Integer>> m = Map.of("c", Map.of("i", 1));
@Comment("e2")
E2 e2 = new E2();
}
@Configuration
static final class E2 {
Map<String, Integer> m = Map.of("i", 1);
@Comment("e3")
E3 e3 = new E3();
@Comment("j")
int j = 10;
}
@Configuration
static final class E3 {
@Comment("i")
int i = 1;
}
@Test
void writeYamlNestedComments1() {
writeConfig(E1.class);
assertFileContentEquals(
"""
# m
m:
c:
i: 1
# e2
e2:
m:
i: 1
# e3
e3:
# i
i: 1
# j
j: 10\
"""
);
}
@Configuration
static final class F1 {
Map<String, Integer> m1 = Map.of("i", 1);
F2 f2 = new F2();
@Comment("f1.m2")
Map<String, Integer> m2 = Map.of("i", 1);
}
@Configuration
static final class F2 {
@Comment("f2.i")
int i;
}
@Test
void writeYamlNestedComments2() {
writeConfig(F1.class);
assertFileContentEquals(
"""
m1:
i: 1
f2:
# f2.i
i: 0
# f1.m2
m2:
i: 1\
"""
);
}
@Configuration
static final class G1 {
@Comment("g1.g2")
G2 g2 = new G2();
}
@Configuration
static final class G2 {
G3 g3 = new G3();
}
@Configuration
static final class G3 {
G4 g4 = new G4();
}
@Configuration
static final class G4 {
@Comment({"g4.g3 1", "g4.g3 2"})
int g3;
@Comment("g4.g4")
int g4;
}
@Test
void writeYamlNestedComments3() {
writeConfig(G1.class);
assertFileContentEquals(
"""
# g1.g2
g2:
g3:
g4:
# g4.g3 1
# g4.g3 2
g3: 0
# g4.g4
g4: 0\
"""
);
}
@Configuration
static final class H1 {
@Comment("h2.1")
H2 h21 = new H2();
@Comment("h2.2")
H2 h22 = null;
}
@Configuration
static final class H2 {
@Comment("j")
int j = 10;
}
@Test
void writeYamlNullFields() {
writeConfig(H1.class);
assertFileContentEquals(
"""
# h2.1
h21:
# j
j: 10\
"""
);
writeConfig(H1.class, builder -> builder.outputNulls(true));
assertFileContentEquals(
"""
# h2.1
h21:
# j
j: 10
# h2.2
h22: null\
"""
);
}
@Configuration
static class J1 {
@Comment("sj1")
String sJ1 = "sj1";
}
static final class J2 extends J1 {
@Comment("sj2")
String sJ2 = "sj2";
}
@Configuration
static class K1 {
@Comment("k1.j1")
J1 k1J1 = new J1();
@Comment("k1.j2")
J2 k1J2 = new J2();
}
static final class K2 extends K1 {
@Comment("k2.j1")
J1 k2J1 = new J1();
@Comment("k2.j2")
J2 k2J2 = new J2();
}
@Test
void writeYamlInheritance() {
writeConfig(K2.class);
assertFileContentEquals(
"""
# k1.j1
k1J1:
# sj1
sJ1: sj1
# k1.j2
k1J2:
# sj1
sJ1: sj1
# sj2
sJ2: sj2
# k2.j1
k2J1:
# sj1
sJ1: sj1
# k2.j2
k2J2:
# sj1
sJ1: sj1
# sj2
sJ2: sj2\
"""
);
}
@Test
void lengthCommonPrefix() {
List<String> ab = List.of("a", "b");
List<String> abc = List.of("a", "b", "c");
List<String> abcd = List.of("a", "b", "c", "d");
List<String> aef = List.of("a", "e", "f");
List<String> def = List.of("d", "e", "f");
assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, ab));
assertEquals(2, YamlFileWriter.lengthCommonPrefix(abc, ab));
assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, abc));
assertEquals(2, YamlFileWriter.lengthCommonPrefix(ab, abcd));
assertEquals(3, YamlFileWriter.lengthCommonPrefix(abc, abc));
assertEquals(3, YamlFileWriter.lengthCommonPrefix(abc, abcd));
assertEquals(1, YamlFileWriter.lengthCommonPrefix(ab, aef));
assertEquals(1, YamlFileWriter.lengthCommonPrefix(abcd, aef));
assertEquals(0, YamlFileWriter.lengthCommonPrefix(ab, def));
assertEquals(0, YamlFileWriter.lengthCommonPrefix(abcd, def));
}
String readFile() {
return TestUtils.readFile(yamlFile);
}
record YamlFileWriterArguments(
String yaml,
Queue<CommentNode> nodes,
YamlConfigurationProperties properties
) {}
void assertFileContentEquals(String expected) {
assertEquals(expected, readFile());
}
void writeConfig(Class<?> cls) {
writeConfig(cls, builder -> {});
}
<T> void writeConfig(Class<T> cls, Consumer<YamlConfigurationProperties.Builder<?>> configurer) {
YamlFileWriterArguments args = argsFromConfig(cls, Reflect.newInstance(cls), configurer);
YamlFileWriter writer = new YamlFileWriter(yamlFile, args.properties);
writer.writeYaml(args.yaml, args.nodes);
}
static <T> YamlFileWriterArguments argsFromConfig(
Class<T> t,
T c,
Consumer<YamlConfigurationProperties.Builder<?>> configurer
) {
YamlConfigurationProperties.Builder<?> builder = YamlConfigurationProperties.newBuilder();
configurer.accept(builder);
YamlConfigurationProperties properties = builder.build();
ConfigurationSerializer<T> serializer = new ConfigurationSerializer<>(t, properties);
Map<?, ?> serialize = serializer.serialize(c);
Dump dump = YamlConfigurationStore.newYamlDumper();
String yaml = dump.dumpToString(serialize);
CommentNodeExtractor extractor = new CommentNodeExtractor(properties);
Queue<CommentNode> nodes = extractor.extractCommentNodes(c);
return new YamlFileWriterArguments(yaml, nodes, properties);
}
}

@ -0,0 +1,188 @@
package de.exlll.configlib.configurations;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.Ignore;
import java.awt.Point;
import java.util.*;
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
@Configuration
public class ExampleConfigurationB1 {
/* IGNORED FIELDS */
private static final int b1_staticFinalInt = 1;
private static int b1_staticInt = 2;
private final int b1_finalInt = 3;
private transient int b1_transientInt = 4;
@Ignore
private int b1_ignoredInt = 5;
@Ignore
private String b1_ignoredString = "ignoredString";
@Ignore
private List<String> b1_ignoredListString = List.of("ignored", "list", "string");
private boolean b1_primBool;
private Character b1_refChar;
private String b1_string;
private List<Byte> b1_listByte;
private Short[] b1_arrayShort;
private Set<Integer> b1_setInteger;
private List<Short> b1_listEmpty;
private Map<Long, Long> b1_mapLongLong;
private List<List<Byte>> b1_listListByte;
private Point b1_point;
public static int getB1_staticFinalInt() {
return b1_staticFinalInt;
}
public static int getB1_staticInt() {
return b1_staticInt;
}
public static void setB1_staticInt(int b1_staticInt) {
ExampleConfigurationB1.b1_staticInt = b1_staticInt;
}
public int getB1_finalInt() {
return b1_finalInt;
}
public int getB1_transientInt() {
return b1_transientInt;
}
public void setB1_transientInt(int b1_transientInt) {
this.b1_transientInt = b1_transientInt;
}
public int getB1_ignoredInt() {
return b1_ignoredInt;
}
public void setB1_ignoredInt(int b1_ignoredInt) {
this.b1_ignoredInt = b1_ignoredInt;
}
public String getB1_ignoredString() {
return b1_ignoredString;
}
public void setB1_ignoredString(String b1_ignoredString) {
this.b1_ignoredString = b1_ignoredString;
}
public List<String> getB1_ignoredListString() {
return b1_ignoredListString;
}
public void setB1_ignoredListString(List<String> b1_ignoredListString) {
this.b1_ignoredListString = b1_ignoredListString;
}
public boolean isB1_primBool() {
return b1_primBool;
}
public void setB1_primBool(boolean b1_primBool) {
this.b1_primBool = b1_primBool;
}
public Character getB1_refChar() {
return b1_refChar;
}
public void setB1_refChar(Character b1_refChar) {
this.b1_refChar = b1_refChar;
}
public String getB1_string() {
return b1_string;
}
public void setB1_string(String b1_string) {
this.b1_string = b1_string;
}
public List<Byte> getB1_listByte() {
return b1_listByte;
}
public void setB1_listByte(List<Byte> b1_listByte) {
this.b1_listByte = b1_listByte;
}
public Short[] getB1_arrayShort() {
return b1_arrayShort;
}
public void setB1_arrayShort(Short[] b1_arrayShort) {
this.b1_arrayShort = b1_arrayShort;
}
public Set<Integer> getB1_setInteger() {
return b1_setInteger;
}
public void setB1_setInteger(Set<Integer> b1_setInteger) {
this.b1_setInteger = b1_setInteger;
}
public List<Short> getB1_listEmpty() {
return b1_listEmpty;
}
public void setB1_listEmpty(List<Short> b1_listEmpty) {
this.b1_listEmpty = b1_listEmpty;
}
public Map<Long, Long> getB1_mapLongLong() {
return b1_mapLongLong;
}
public void setB1_mapLongLong(Map<Long, Long> b1_mapLongLong) {
this.b1_mapLongLong = b1_mapLongLong;
}
public List<List<Byte>> getB1_listListByte() {
return b1_listListByte;
}
public void setB1_listListByte(List<List<Byte>> b1_listListByte) {
this.b1_listListByte = b1_listListByte;
}
public Point getB1_point() {
return b1_point;
}
public void setB1_point(Point b1_point) {
this.b1_point = b1_point;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExampleConfigurationB1 that = (ExampleConfigurationB1) o;
return b1_transientInt == that.b1_transientInt &&
b1_ignoredInt == that.b1_ignoredInt &&
b1_primBool == that.b1_primBool &&
Objects.equals(b1_ignoredString, that.b1_ignoredString) &&
Objects.equals(b1_ignoredListString, that.b1_ignoredListString) &&
Objects.equals(b1_refChar, that.b1_refChar) &&
Objects.equals(b1_string, that.b1_string) &&
Objects.equals(b1_listByte, that.b1_listByte) &&
Arrays.equals(b1_arrayShort, that.b1_arrayShort) &&
Objects.equals(b1_setInteger, that.b1_setInteger) &&
Objects.equals(b1_listEmpty, that.b1_listEmpty) &&
Objects.equals(b1_mapLongLong, that.b1_mapLongLong) &&
Objects.equals(b1_listListByte, that.b1_listListByte) &&
Objects.equals(b1_point, that.b1_point);
}
@Override
public int hashCode() {
return 0;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save