Update ConfigLib to version 2.0.0

dev
Exlll 6 years ago
parent e1b629d5df
commit 49e5b56b5c

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

@ -0,0 +1,56 @@
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 +1,5 @@
name: ConfigLib
author: Exlll
version: 1.4.1
version: 2.0.0
main: de.exlll.configlib.ConfigLib

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

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

@ -1,71 +0,0 @@
package de.exlll.configlib;
import java.util.Collections;
import java.util.List;
import java.util.Map;
final class CommentAdder {
private final Comments comments;
private StringBuilder builder;
CommentAdder(Comments comments) {
this.comments = comments;
}
String addComments(String serializedMap) {
builder = new StringBuilder();
addClassComments();
addMap(serializedMap);
return builder.toString();
}
private void addClassComments() {
List<String> clsComments = comments.getClassComments();
if (!clsComments.isEmpty()) {
addComments(clsComments);
builder.append('\n');
}
}
private void addComments(List<String> comments) {
for (String comment : comments) {
if (!comment.trim().isEmpty()) {
builder.append("# ");
builder.append(comment);
}
builder.append('\n');
}
}
private void addMap(String map) {
String[] lines = map.split("\n");
for (String line : lines) {
addLine(line);
}
}
private void addLine(String line) {
addLineComments(line);
builder.append(line);
builder.append('\n');
}
private void addLineComments(String line) {
if (!line.contains(":")) {
return;
}
List<String> comments = getLineComments(line);
addComments(comments);
}
private List<String> getLineComments(String line) {
Map<String, List<String>> entries = comments.getFieldComments();
for (Map.Entry<String, List<String>> entry : entries.entrySet()) {
String s = entry.getKey() + ":";
if (line.startsWith(s)) {
return entry.getValue();
}
}
return Collections.emptyList();
}
}

@ -1,50 +1,84 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.Comment;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.toMap;
final class Comments {
/**
* 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;
Comments(Class<?> cls) {
this.classComments = getComments(cls);
this.fieldComments = getFieldComments(cls);
addVersionComments(cls);
private Comments(List<String> classComments,
Map<String, List<String>> fieldComments) {
this.classComments = classComments;
this.fieldComments = fieldComments;
}
private List<String> getComments(AnnotatedElement element) {
Comment comment = element.getAnnotation(Comment.class);
return (comment != null) ? Arrays.asList(comment.value()) :
Collections.emptyList();
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 Map<String, List<String>> getFieldComments(Class<?> cls) {
return Arrays.stream(cls.getDeclaredFields())
.filter(this::hasCommentAnnotation)
.collect(toMap(Field::getName, this::getComments));
private static boolean isCommented(AnnotatedElement element) {
return element.isAnnotationPresent(Comment.class);
}
private void addVersionComments(Class<?> cls) {
final Version version = Reflect.getVersion(cls);
if (version != null) {
final String vfn = version.fieldName();
final List<String> vfc = Arrays.asList(version.fieldComments());
fieldComments.put(vfn, vfc);
}
private static List<String> getComments(AnnotatedElement element) {
Comment comment = element.getAnnotation(Comment.class);
return (comment != null)
? List.of(comment.value())
: Collections.emptyList();
}
private boolean hasCommentAnnotation(AnnotatedElement element) {
return element.isAnnotationPresent(Comment.class);
/**
* 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,7 +0,0 @@
package de.exlll.configlib;
public final class ConfigException extends RuntimeException {
public ConfigException(String message) {
super(message);
}
}

@ -1,204 +0,0 @@
package de.exlll.configlib;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
public final class ConfigList<T> implements Defaultable<List<?>>, List<T>, RandomAccess {
private final Class<T> cls;
private final List<T> list;
public ConfigList(Class<T> cls) {
Objects.requireNonNull(cls);
if (!Reflect.isSimpleType(cls)) {
Reflect.checkDefaultConstructor(cls);
}
this.cls = cls;
this.list = Collections.checkedList(new ArrayList<>(), cls);
}
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<T> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T1> T1[] toArray(T1[] a) {
return list.toArray(a);
}
@Override
public boolean add(T t) {
return list.add(t);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends T> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public T get(int index) {
return list.get(index);
}
@Override
public T set(int index, T element) {
return list.set(index, element);
}
@Override
public void add(int index, T element) {
list.add(index, element);
}
@Override
public T remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<T> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<T> listIterator(int index) {
return list.listIterator(index);
}
@Override
public List<T> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
@Override
public void replaceAll(UnaryOperator<T> operator) {
list.replaceAll(operator);
}
@Override
public void sort(Comparator<? super T> c) {
list.sort(c);
}
@Override
public Spliterator<T> spliterator() {
return list.spliterator();
}
@Override
public boolean removeIf(Predicate<? super T> filter) {
return list.removeIf(filter);
}
@Override
public Stream<T> stream() {
return list.stream();
}
@Override
public Stream<T> parallelStream() {
return list.parallelStream();
}
@Override
public void forEach(Consumer<? super T> action) {
list.forEach(action);
}
@Override
public String toString() {
return "ConfigList{" +
"cls=" + cls +
", list=" + list +
'}';
}
@Override
public List<?> toDefault() {
if (Reflect.isSimpleType(cls)) {
return new ArrayList<>(list);
}
List<Object> l = new ArrayList<>();
for (Object item : list) {
l.add(FieldMapper.instanceToMap(item));
}
return l;
}
@Override
public void fromDefault(Object value) {
clear();
for (Object item : (List<?>) value) {
Object instance = fromDefault(item, cls);
add(cls.cast(instance));
}
}
List<T> getList() {
return list;
}
}

@ -1,184 +0,0 @@
package de.exlll.configlib;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
public final class ConfigMap<K, V> implements Defaultable<Map<K, ?>>, Map<K, V> {
private final Class<K> keyClass;
private final Class<V> valueClass;
private final Map<K, V> map;
public ConfigMap(Class<K> keyClass, Class<V> valueClass) {
Objects.requireNonNull(keyClass);
Objects.requireNonNull(valueClass);
checkSimpleTypeKey(keyClass);
if (!Reflect.isSimpleType(valueClass)) {
Reflect.checkDefaultConstructor(valueClass);
}
this.keyClass = keyClass;
this.valueClass = valueClass;
this.map = Collections.checkedMap(
new LinkedHashMap<>(), keyClass, valueClass
);
}
private void checkSimpleTypeKey(Class<?> keyClass) {
if (!Reflect.isSimpleType(keyClass)) {
String msg = "Class " + keyClass.getSimpleName() + " is not a simple type.\n" +
"Only simple types can be used as keys in a map.";
throw new IllegalArgumentException(msg);
}
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public V get(Object key) {
return map.get(key);
}
@Override
public V put(K key, V value) {
return map.put(key, value);
}
@Override
public V remove(Object key) {
return map.remove(key);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
map.putAll(m);
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<K> keySet() {
return map.keySet();
}
@Override
public Collection<V> values() {
return map.values();
}
@Override
public Set<Entry<K, V>> entrySet() {
return map.entrySet();
}
@Override
public V getOrDefault(Object key, V defaultValue) {
return map.getOrDefault(key, defaultValue);
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
map.forEach(action);
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
map.replaceAll(function);
}
@Override
public V putIfAbsent(K key, V value) {
return map.putIfAbsent(key, value);
}
@Override
public boolean remove(Object key, Object value) {
return map.remove(key, value);
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
return map.replace(key, oldValue, newValue);
}
@Override
public V replace(K key, V value) {
return map.replace(key, value);
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
return map.computeIfAbsent(key, mappingFunction);
}
@Override
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return map.computeIfPresent(key, remappingFunction);
}
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
return map.compute(key, remappingFunction);
}
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
return map.merge(key, value, remappingFunction);
}
@Override
public String toString() {
return "ConfigMap{" +
"valueClass=" + valueClass +
", map=" + map +
'}';
}
@Override
public Map<K, ?> toDefault() {
if (Reflect.isSimpleType(valueClass)) {
return new LinkedHashMap<>(map);
}
Map<K, Object> m = new LinkedHashMap<>();
for (Entry<K, V> entry : entrySet()) {
Object mapped = FieldMapper.instanceToMap(entry.getValue());
m.put(entry.getKey(), mapped);
}
return m;
}
@Override
public void fromDefault(Object value) {
clear();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
Object instance = fromDefault(entry.getValue(), valueClass);
Reflect.checkType(entry.getKey(), keyClass);
put(keyClass.cast(entry.getKey()), valueClass.cast(instance));
}
}
Map<K, V> getMap() {
return map;
}
}

@ -1,20 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;
enum ConfigReader {
;
static String read(Path path) throws IOException {
try (Scanner scanner = new Scanner(path)) {
scanner.useDelimiter("\\z");
StringBuilder builder = new StringBuilder();
while (scanner.hasNext()) {
builder.append(scanner.next());
}
return builder.toString();
}
}
}

@ -1,143 +0,0 @@
package de.exlll.configlib;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
public final class ConfigSet<T> implements Defaultable<Set<?>>, Set<T> {
private final Class<T> cls;
private final Set<T> set;
public ConfigSet(Class<T> cls) {
Objects.requireNonNull(cls);
if (!Reflect.isSimpleType(cls)) {
Reflect.checkDefaultConstructor(cls);
}
this.cls = cls;
this.set = Collections.checkedSet(new LinkedHashSet<>(), cls);
}
@Override
public int size() {
return set.size();
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@Override
public Iterator<T> iterator() {
return set.iterator();
}
@Override
public void forEach(Consumer<? super T> action) {
set.forEach(action);
}
@Override
public Object[] toArray() {
return set.toArray();
}
@Override
public <T1> T1[] toArray(T1[] a) {
return set.toArray(a);
}
@Override
public boolean add(T t) {
return set.add(t);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends T> c) {
return set.addAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return set.retainAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public boolean removeIf(Predicate<? super T> filter) {
return set.removeIf(filter);
}
@Override
public void clear() {
set.clear();
}
@Override
public Spliterator<T> spliterator() {
return set.spliterator();
}
@Override
public Stream<T> stream() {
return set.stream();
}
@Override
public Stream<T> parallelStream() {
return set.parallelStream();
}
@Override
public String toString() {
return "ConfigSet{" +
"cls=" + cls +
", set=" + set +
'}';
}
@Override
public Set<?> toDefault() {
if (Reflect.isSimpleType(cls)) {
return new LinkedHashSet<>(set);
}
Set<Object> s = new LinkedHashSet<>();
for (Object item : set) {
s.add(FieldMapper.instanceToMap(item));
}
return s;
}
@Override
public void fromDefault(Object value) {
clear();
for (Object item : (Set<?>) value) {
Object instance = fromDefault(item, cls);
add(cls.cast(instance));
}
}
Set<T> getSet() {
return set;
}
}

@ -1,16 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
enum ConfigWriter {
;
static void write(Path path, String text) throws IOException {
try (Writer writer = Files.newBufferedWriter(path)) {
writer.write(text);
}
}
}

@ -1,263 +1,174 @@
package de.exlll.configlib;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.parser.ParserException;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import de.exlll.configlib.format.FieldNameFormatter;
import de.exlll.configlib.format.FieldNameFormatters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Map;
public abstract class Configuration {
private final Path configPath;
private final CommentAdder adder;
private YamlSerializer serializer;
import java.util.*;
/**
* 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>> {
/**
* Constructs a new {@code Configuration} instance.
* <p>
* You can use {@link java.io.File#toPath()} get a {@link Path} object
* from a {@link java.io.File}.
*
* @param configPath location of the configuration file
* @throws NullPointerException if {@code configPath} is null
* {@code Comments} object containing all class and field comments
* of this configuration
*/
protected Configuration(Path configPath) {
this.configPath = configPath;
this.adder = new CommentAdder(new Comments(getClass()));
}
private void initSerializer() {
if (serializer == null) {
this.serializer = new YamlSerializer(
createConstructor(), createRepresenter(),
createDumperOptions(), createResolver()
);
}
}
protected final Comments comments;
private final Properties props;
/**
* Loads {@code this} configuration from a configuration file. The file is
* located at the path pointed to by the {@code Path} object used to create
* {@code this} instance.
* <p>
* The values of the fields of this instance are updated as follows:<br>
* For each non-{@code final}, non-{@code static} and non-{@code transient}
* field of {@code this} configuration instance: <br>
* - If the field's value is null, throw a {@code NullPointerException} <br>
* - If the file contains the field's name, update the field's value with
* the value from the file. Otherwise, keep the default value. <br>
* This algorithm is applied recursively for any non-default field.
* Constructs a new {@code Configuration} object.
*
* @throws ClassCastException if parsed Object is not a {@code Map}
* @throws IOException if an I/O error occurs when loading the configuration file
* @throws NullPointerException if a value of a field of this instance is null
* @throws ParserException if invalid YAML
* @param properties {@code Properties} used to configure this configuration
* @throws NullPointerException if {@code properties} is null
*/
public final void load() throws IOException {
Map<String, Object> deserializedMap = readAndDeserialize();
FieldMapper.instanceFromMap(this, deserializedMap);
postLoadHook();
}
private Map<String, Object> readAndDeserialize() throws IOException {
initSerializer();
String yaml = ConfigReader.read(configPath);
return serializer.deserialize(yaml);
protected Configuration(Properties properties) {
this.props = Objects.requireNonNull(properties);
this.comments = Comments.ofClass(getClass());
}
/**
* Saves the comments of {@code this} class as well as the names,
* values and comments of its non-{@code final}, non-{@code static}
* and non-{@code transient} fields to a configuration file. If this
* class uses versioning, the current version is saved, too.
* <p>
* The file used to save this configuration is located at the path pointed
* to by the {@code Path} object used to create {@code this} instance.
* <p>
* The default algorithm used to save {@code this} configuration to a file
* is as follows:<br>
* <ol>
* <li>If the file doesn't exist, it is created.</li>
* <li>For each non-{@code final}, non-{@code static} and non-{@code transient}
* field of {@code this} configuration instance:
* <ul>
* <li>If the file doesn't contain the field's name, the field's name and
* value are added. Otherwise, the value is simply updated.</li>
* </ul>
* </li>
* <li>If the file contains field names that don't match any name of a field
* of this class, the file's field names together with their values are
* removed from the file.</li>
* <li>(only with versioning) The current version is updated.</li>
* </ol>
* The default behavior can be overridden using <i>versioning</i>.
* Saves this {@code Configuration}.
*
* @throws ConfigException if a name clash between a field name and the version
* field name occurs (can only happen if versioning is used)
* @throws NoSuchFileException if the old version contains illegal file path characters
* @throws IOException if an I/O error occurs when saving the configuration file
* @throws ParserException if invalid YAML
* @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() throws IOException {
initSerializer();
createParentDirectories();
Map<String, Object> map = FieldMapper.instanceToMap(this);
version(map);
String serializedMap = serializer.serialize(map);
ConfigWriter.write(configPath, adder.addComments(serializedMap));
}
private void version(Map<String, Object> map) throws IOException {
final Version version = Reflect.getVersion(getClass());
if (version == null) {
return;
}
final String vfn = version.fieldName();
if (map.containsKey(vfn)) {
String msg = "Problem: Configuration '" + this + "' cannot be " +
"saved because one its fields has the same name as the " +
"version field: '" + vfn + "'.\nSolution: Rename the " +
"field or use a different version field name.";
throw new ConfigException(msg);
public final void save() {
try {
preSave();
Map<String, Object> map = FieldMapper.instanceToMap(this, props);
getSource().saveConfiguration(getThis(), map);
} catch (IOException e) {
throw new ConfigurationStoreException(e);
}
map.put(vfn, version.version());
version.updateStrategy().update(this, version);
}
private void createParentDirectories() throws IOException {
Files.createDirectories(configPath.getParent());
}
/**
* Loads and saves {@code this} configuration.
* <p>
* This method first calls {@link #load()} and then {@link #save()}.
* Loads this {@code Configuration}.
*
* @throws ClassCastException if parsed Object is not a {@code Map}
* @throws IOException if an I/O error occurs when loading or saving the configuration file
* @throws NullPointerException if a value of a field of this instance is null
* @throws ParserException if invalid YAML
* @see #load()
* @see #save()
* @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 loadAndSave() throws IOException {
public final void load() {
try {
load();
save();
} catch (NoSuchFileException e) {
postLoadHook();
save();
Map<String, Object> map = getSource().loadConfiguration(getThis());
FieldMapper.instanceFromMap(this, map, props);
postLoad();
} catch (IOException e) {
throw new ConfigurationStoreException(e);
}
}
/**
* Protected method invoked after all fields have successfully been loaded.
* <p>
* The default implementation of this method does nothing. Subclasses may
* override this method in order to execute some action after all fields
* have successfully been loaded.
* Returns the {@link ConfigurationSource} used for saving and loading this
* {@code Configuration}.
*
* @return {@code ConfigurationSource} used for saving and loading
*/
protected void postLoadHook() {}
protected abstract ConfigurationSource<C> getSource();
/**
* Returns a {@link BaseConstructor} which is used to configure a
* {@link Yaml} object.
* <p>
* Override this method to change the way the {@code Yaml} object is created.
* <p>
* This method may not return null.
* Returns this {@code Configuration}.
*
* @return a {@code BaseConstructor} object
* @see org.yaml.snakeyaml.constructor.BaseConstructor
* @see #createRepresenter()
* @see #createDumperOptions()
* @see #createResolver()
* @return this {@code Configuration}
*/
protected BaseConstructor createConstructor() {
return new Constructor();
}
protected abstract C getThis();
/**
* Returns a {@link Representer} which is used to configure a {@link Yaml}
* object.
* <p>
* Override this method to change the way the {@code Yaml} object is created.
* Hook that is executed right before this {@code Configuration} is saved.
* <p>
* This method may not return null.
*
* @return a {@code Representer} object
* @see org.yaml.snakeyaml.representer.Representer
* @see #createConstructor()
* @see #createDumperOptions()
* @see #createResolver()
* The default implementation of this method does nothing.
*/
protected Representer createRepresenter() {
return new Representer();
}
protected void preSave() {}
/**
* Returns a {@link DumperOptions} object which is used to configure a
* {@link Yaml} object.
* <p>
* Override this method to change the way the {@code Yaml} object is created.
* Hook that is executed right after this {@code Configuration} has
* successfully been loaded.
* <p>
* This method may not return null.
*
* @return a {@code DumperOptions} object
* @see org.yaml.snakeyaml.DumperOptions
* @see #createConstructor()
* @see #createRepresenter()
* @see #createResolver()
* The default implementation of this method does nothing.
*/
protected DumperOptions createDumperOptions() {
DumperOptions options = new DumperOptions();
options.setIndent(2);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
return options;
}
protected void postLoad() {}
/**
* Returns a {@link Resolver} which is used to configure a {@link Yaml} object.
* <p>
* Override this method to change the way the {@code Yaml} object is created.
* <p>
* This method may not return null.
*
* @return a {@code Resolver} object
* @see org.yaml.snakeyaml.resolver.Resolver
* @see #createConstructor()
* @see #createRepresenter()
* @see #createDumperOptions()
* Instances of a {@code Properties} class are used to configure different
* aspects of a configuration.
*/
protected Resolver createResolver() {
return new Resolver();
}
protected static class Properties {
private final FieldNameFormatter formatter;
/**
* 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;
}
final String currentFileVersion() throws IOException {
final Version version = Reflect.getVersion(getClass());
return (version == null) ? null : readCurrentFileVersion(version);
}
static Builder<?> builder() {
return new Builder() {
@Override
protected Builder<?> getThis() {
return this;
}
};
}
private String readCurrentFileVersion(Version version) throws IOException {
try {
final Map<String, Object> map = readAndDeserialize();
return (String) map.get(version.fieldName());
} catch (NoSuchFileException ignored) {
/* there is no file version if the file doesn't exist */
return null;
/**
* Returns the {@code FieldNameFormatter} of a configuration.
*
* @return {@code FieldNameFormatter} of a configuration
*/
public final FieldNameFormatter getFormatter() {
return formatter;
}
}
final Path getPath() {
return configPath;
/**
* 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;
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 ist null}
*/
public final B setFormatter(FieldNameFormatter formatter) {
this.formatter = Objects.requireNonNull(formatter);
return getThis();
}
/**
* Builds a new {@code Properties} instance using the values set.
*
* @return new {@code Properties} instance
*/
public Properties build() {
return new Properties(this);
}
}
}
}

@ -0,0 +1,16 @@
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.
*/
public final class ConfigurationException extends RuntimeException {
ConfigurationException(String message) {
super(message);
}
ConfigurationException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,32 @@
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;
}

@ -0,0 +1,10 @@
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);
}
}

@ -0,0 +1,196 @@
package de.exlll.configlib;
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 <F> the type of the field value
* @param <T> the type of the converted value
*/
public interface Converter<F, 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} will be thrown.
*
* @param element field value that is converted
* @param info information about the current conversion step
* @return converted field value
*/
T convertTo(F 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
*/
F 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 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 ConversionInfo(Field field, Object instance, Object mapValue,
Configuration.Properties props) {
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 = props;
this.elementType = elementType(field);
}
private static Class<?> elementType(Field field) {
if (field.isAnnotationPresent(ElementType.class)) {
ElementType et = field.getAnnotation(ElementType.class);
return et.value();
}
return null;
}
static ConversionInfo of(Field field, Object instance,
Configuration.Properties props) {
return new ConversionInfo(field, instance, null, props);
}
static ConversionInfo of(Field field, Object instance, Object mapValue,
Configuration.Properties props) {
return new ConversionInfo(field, instance, mapValue, props);
}
/**
* 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;
}
}
}

@ -0,0 +1,529 @@
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
) {
return o -> selectNonSimpleConverter(element.getClass(), info)
.convertTo(o, info);
}
private static Function<Object, ?> createFromConversionFunction(
Object element, ConversionInfo info
) {
if ((element instanceof Map<?, ?>) && isTypeMap((Map<?, ?>) element)) {
return o -> {
Map<String, Object> map = toTypeMap(o, null);
Object inst = Reflect.newInstance(info.getElementType());
FieldMapper.instanceFromMap(inst, map, info.getProperties());
return inst;
};
} else {
return o -> selectNonSimpleConverter(element.getClass(), info)
.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.getProperties());
}
@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.getProperties());
return newInstance;
}
@Override
public void preConvertFrom(ConversionInfo info) {
checkTypeHasNoArgsConstructor(info);
checkTypeIsConfigurationElement(info.getValueType(), info.getFieldName());
}
}
}

@ -1,23 +0,0 @@
package de.exlll.configlib;
import java.util.Map;
interface Defaultable<T> {
T toDefault();
void fromDefault(Object value);
default Object fromDefault(final Object instance, Class<?> cls) {
Object newInstance = instance;
if (!Reflect.isSimpleType(cls)) {
Reflect.checkType(instance, Map.class);
Reflect.checkMapEntries((Map<?, ?>) instance, String.class, Object.class);
@SuppressWarnings("unchecked")
Map<String, ?> map = (Map<String, ?>) instance;
newInstance = Reflect.newInstance(cls);
FieldMapper.instanceFromMap(newInstance, map);
}
Reflect.checkType(newInstance, cls);
return newInstance;
}
}

@ -1,24 +0,0 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.function.Predicate;
enum FieldFilter implements Predicate<Field> {
INSTANCE;
/**
* Tests if a field is not final, static, synthetic and transient.
*
* @param field Field that is tested
* @return true if {@code field} is not final, static, synthetic or transient
*/
@Override
public boolean test(Field field) {
int mods = field.getModifiers();
boolean fst = Modifier.isFinal(mods) ||
Modifier.isStatic(mods) ||
Modifier.isTransient(mods);
return !fst && !field.isSynthetic();
}
}

@ -1,75 +1,105 @@
package de.exlll.configlib;
import de.exlll.configlib.Converter.ConversionInfo;
import de.exlll.configlib.format.FieldNameFormatter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import static de.exlll.configlib.Validator.*;
import static java.util.stream.Collectors.toList;
enum FieldMapper {
;
static Map<String, Object> instanceToMap(Object instance) {
static Map<String, Object> instanceToMap(
Object inst, Configuration.Properties props
) {
Map<String, Object> map = new LinkedHashMap<>();
FilteredFields ff = FilteredFields.of(instance.getClass());
for (Field field : ff) {
Object value = Reflect.getValue(field, instance);
checkNull(field, value);
value = toSerializableObject(value);
map.put(field.getName(), value);
for (Field field : FieldFilter.filterFields(inst.getClass())) {
Object val = toConvertibleObject(field, inst, props);
FieldNameFormatter fnf = props.getFormatter();
String fn = fnf.fromFieldName(field.getName());
map.put(fn, val);
}
return map;
}
static Object toSerializableObject(Object input) {
if (input instanceof Defaultable<?>) {
return ((Defaultable<?>) input).toDefault();
} else if (Reflect.isDefault(input.getClass())) {
return input;
} else {
return instanceToMap(input);
}
private static Object toConvertibleObject(
Field field, Object instance, Configuration.Properties props
) {
checkDefaultValueNull(field, instance);
ConversionInfo info = ConversionInfo.of(field, instance, props);
checkFieldWithElementTypeIsContainer(info);
Object converted = Converters.convertTo(info);
checkConverterNotReturnsNull(converted, info);
return converted;
}
static void instanceFromMap(Object instance, Map<String, ?> map) {
FilteredFields ff = FilteredFields.of(instance.getClass());
for (Field field : ff) {
Object value = map.get(field.getName());
fromSerializedObject(field, instance, value);
static void instanceFromMap(
Object inst, Map<String, Object> instMap,
Configuration.Properties props
) {
for (Field field : FieldFilter.filterFields(inst.getClass())) {
FieldNameFormatter fnf = props.getFormatter();
String fn = fnf.fromFieldName(field.getName());
Object mapValue = instMap.get(fn);
if (mapValue != null) {
fromConvertedObject(field, inst, mapValue, props);
}
}
}
static void fromSerializedObject(Field field, Object instance, Object serialized) {
if (serialized == null) {
return; // keep default value
private static void fromConvertedObject(
Field field, Object instance, Object mapValue,
Configuration.Properties props
) {
checkDefaultValueNull(field, instance);
ConversionInfo info = ConversionInfo.of(field, instance, mapValue, props);
checkFieldWithElementTypeIsContainer(info);
Object convert = Converters.convertFrom(info);
if (convert == null) {
return;
}
Object fieldValue = Reflect.getValue(field, instance);
checkNull(field, fieldValue);
if (fieldValue instanceof Defaultable<?>) {
((Defaultable<?>) fieldValue).fromDefault(serialized);
} else if (Reflect.isDefault(field.getType())) {
Reflect.setValue(field, instance, serialized);
} else {
instanceFromMap(fieldValue, castToMap(serialized));
if (Reflect.isContainerType(info.getFieldType())) {
checkFieldTypeAssignableFrom(convert.getClass(), info);
}
Reflect.setValue(field, instance, convert);
}
private static void checkNull(Field field, Object o) {
if (o == null) {
String msg = "The value of field " + field.getName() + " is null.\n" +
"Please assign a non-null default value or remove this field.";
throw new NullPointerException(msg);
}
private static void checkDefaultValueNull(Field field, Object instance) {
Object val = Reflect.getValue(field, instance);
checkNotNull(val, field.getName());
}
enum FieldFilter implements Predicate<Field> {
DEFAULT;
private static Map<String, Object> castToMap(Object mapObject) {
Reflect.checkType(mapObject, Map.class);
Reflect.checkMapEntries((Map<?, ?>) mapObject, String.class, Object.class);
@SuppressWarnings("unchecked")
Map<String, Object> m = (Map<String, Object>) mapObject;
return m;
static List<Field> filterFields(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
return Arrays.stream(fields)
.filter(DEFAULT)
.collect(toList());
}
@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,32 +0,0 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
final class FilteredFields implements Iterable<Field> {
private final List<Field> filteredFields;
FilteredFields(Field[] fields, Predicate<Field> filter) {
Objects.requireNonNull(fields);
Objects.requireNonNull(filter);
this.filteredFields = Arrays.stream(fields)
.filter(filter)
.collect(Collectors.toList());
}
static FilteredFields of(Class<?> cls) {
Field[] fields = cls.getDeclaredFields();
return new FilteredFields(fields, FieldFilter.INSTANCE);
}
@Override
public Iterator<Field> iterator() {
return filteredFields.iterator();
}
}

@ -1,28 +1,33 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.ConfigurationElement;
import de.exlll.configlib.annotation.Convert;
import de.exlll.configlib.annotation.NoConvert;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
enum Reflect {
;
private static final Class<?>[] SIMPLE_TYPES = {
Boolean.class, String.class, Character.class,
Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class
};
private static final Set<Class<?>> simpleTypes = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(SIMPLE_TYPES))
private static final Set<Class<?>> SIMPLE_TYPES = Set.of(
Boolean.class,
Byte.class,
Character.class,
Short.class,
Integer.class,
Long.class,
Float.class,
Double.class,
String.class
);
static boolean isDefault(Class<?> cls) {
// default classes can be properly serialized by default
return isSimpleType(cls) || isContainerType(cls);
}
static boolean isSimpleType(Class<?> cls) {
return cls.isPrimitive() || simpleTypes.contains(cls);
return cls.isPrimitive() || SIMPLE_TYPES.contains(cls);
}
static boolean isContainerType(Class<?> cls) {
@ -31,82 +36,77 @@ enum Reflect {
Map.class.isAssignableFrom(cls);
}
static Object newInstance(Class<?> cls) {
checkDefaultConstructor(cls);
Constructor<?> constructor = getDefaultConstructor(cls);
constructor.setAccessible(true);
return newInstance(constructor);
static boolean isEnumType(Class<?> cls) {
return cls.isEnum();
}
private static Object newInstance(Constructor<?> constructor) {
try {
return constructor.newInstance();
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
static boolean hasConverter(Field field) {
return field.isAnnotationPresent(Convert.class);
}
static void checkDefaultConstructor(Class<?> cls) {
if (!hasDefaultConstructor(cls)) {
String msg = "Class " + cls.getSimpleName() + " doesn't have a default constructor.";
throw new IllegalArgumentException(msg);
}
static boolean hasNoConvert(Field field) {
return field.isAnnotationPresent(NoConvert.class);
}
static Constructor<?> getDefaultConstructor(Class<?> cls) {
static <T> T newInstance(Class<T> cls) {
try {
return cls.getDeclaredConstructor();
Constructor<T> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
/* This exception should not be thrown because we check
* the presence of a no-args constructor elsewhere. */
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) {
/* This exception should not be thrown because
* we call this method only for concrete types. */
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 boolean hasDefaultConstructor(Class<?> cls) {
return Arrays.stream(cls.getDeclaredConstructors())
.anyMatch(cst -> cst.getParameterCount() == 0);
}
static Object getValue(Field field, Object instance) {
static Object getValue(Field field, Object inst) {
try {
field.setAccessible(true);
return field.get(instance);
return field.get(inst);
} catch (IllegalAccessException e) {
/* This exception is never thrown because we filter
* inaccessible fields out. */
throw new RuntimeException(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 instance, Object value) {
static void setValue(Field field, Object inst, Object value) {
try {
field.setAccessible(true);
value = TypeConverter.convertValue(field.getType(), value);
field.set(instance, value);
field.set(inst, value);
} catch (IllegalAccessException e) {
/* This exception is never thrown because we filter
* inaccessible fields out. */
throw new RuntimeException(e);
}
}
static void checkType(Object object, Class<?> expectedType) {
if (!expectedType.isAssignableFrom(object.getClass())) {
String clsName = object.getClass().getSimpleName();
String msg = "Invalid type!\n" +
"Object '" + object + "' is of type " + clsName + ". " +
"Expected type: " + expectedType.getSimpleName();
throw new IllegalArgumentException(msg);
String msg = "Illegal access of field '" + field + "' " +
"on object " + inst + ".";
throw new ConfigurationException(msg, e);
}
}
static void checkMapEntries(Map<?, ?> map, Class<?> keyClass, Class<?> valueClass) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
checkType(entry.getKey(), keyClass);
checkType(entry.getValue(), valueClass);
}
static boolean isConfigurationElement(Class<?> cls) {
return cls.isAnnotationPresent(ConfigurationElement.class);
}
static Version getVersion(Class<?> cls) {
return cls.getAnnotation(Version.class);
static boolean hasNoArgConstructor(Class<?> cls) {
return Arrays.stream(cls.getDeclaredConstructors())
.anyMatch(c -> c.getParameterCount() == 0);
}
}

@ -1,85 +0,0 @@
package de.exlll.configlib;
import java.util.Objects;
enum TypeConverter {
;
static Object convertValue(Class<?> target, Object value) {
Objects.requireNonNull(target);
Objects.requireNonNull(value);
if (target == value.getClass()) {
return value;
}
if (value instanceof Number) {
return convertNumber(target, (Number) value);
}
if (value instanceof String) {
return convertString((String) value);
}
return value;
}
static Character convertString(String s) {
int len = s.length();
if (len != 1) {
String msg = "String '" + s + "' cannot be converted to a character." +
" Length of s: " + len + " Required length: 1";
throw new IllegalArgumentException(msg);
}
return s.charAt(0);
}
static Number convertNumber(Class<?> targetType, Number number) {
if (isByteClass(targetType)) {
return number.byteValue();
} else if (isShortClass(targetType)) {
return number.shortValue();
} else if (isIntegerClass(targetType)) {
return number.intValue();
} else if (isLongClass(targetType)) {
return number.longValue();
} else if (isFloatClass(targetType)) {
return number.floatValue();
} else if (isDoubleClass(targetType)) {
return number.doubleValue();
} else {
String msg = "Number cannot be converted to target type " +
"'" + targetType + "'";
throw new IllegalArgumentException(msg);
}
}
static boolean isBooleanClass(Class<?> cls) {
return (cls == Boolean.class) || (cls == Boolean.TYPE);
}
static boolean isByteClass(Class<?> cls) {
return (cls == Byte.class) || (cls == Byte.TYPE);
}
static boolean isShortClass(Class<?> cls) {
return (cls == Short.class) || (cls == Short.TYPE);
}
static boolean isIntegerClass(Class<?> cls) {
return (cls == Integer.class) || (cls == Integer.TYPE);
}
static boolean isLongClass(Class<?> cls) {
return (cls == Long.class) || (cls == Long.TYPE);
}
static boolean isFloatClass(Class<?> cls) {
return (cls == Float.class) || (cls == Float.TYPE);
}
static boolean isDoubleClass(Class<?> cls) {
return (cls == Double.class) || (cls == Double.TYPE);
}
static boolean isCharacterClass(Class<?> cls) {
return (cls == Character.class) || (cls == Character.TYPE);
}
}

@ -1,50 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.nio.file.*;
/**
* The constants of this enumerated type describe strategies used to update
* different versions of configuration files. The strategies are only applied
* if a version change is detected.
*/
public enum UpdateStrategy {
/**
* Updates the configuration file using the default strategy described at
* {@link Configuration#save()}.
*/
DEFAULT {
@Override
void update(Configuration config, Version version) {}
},
/**
* Updates the configuration file using the {@link #DEFAULT} strategy.
* Before the configuration is updated a copy of its current version is
* saved. If the configuration uses versioning for the first time, the
* copy is named "&lt;filename&gt;-old". Otherwise, the old version is
* appended to the file name: "&lt;filename&gt;-v&lt;old version&gt;".
*
* @see UpdateStrategy#DEFAULT
*/
DEFAULT_RENAME {
@Override
void update(Configuration config, Version version) throws IOException {
final Path path = config.getPath();
if (!Files.exists(path)) {
return;
}
final String fileVersion = config.currentFileVersion();
if (!version.version().equals(fileVersion)) {
final FileSystem fs = path.getFileSystem();
final String v = (fileVersion == null) ? "-old" : "-v" + fileVersion;
final String fn = path.toString() + v;
Files.move(path, fs.getPath(fn), StandardCopyOption.REPLACE_EXISTING);
}
}
};
abstract void update(Configuration config, Version version)
throws IOException;
}

@ -0,0 +1,334 @@
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<?> convClass, String fn) {
if (!Reflect.hasNoArgConstructor(convClass)) {
String msg = "Converter '" + convClass.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 boolean isTypeMap(Map<?, ?> map) {
return map.entrySet().stream().allMatch(entry -> {
Class<?> keyCls = entry.getKey().getClass();
Class<?> valCls = entry.getValue().getClass();
return (String.class == keyCls) && Reflect.isSimpleType(valCls);
});
}
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 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,45 +0,0 @@
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 {@link Configuration} has a version.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Version {
/**
* Returns the current version of a configuration.
*
* @return current configuration version
*/
String version() default "1.0.0";
/**
* Returns the name of the version field.
*
* @return name of the version field
*/
String fieldName() default "version";
/**
* Returns the comments describing the version field.
*
* @return comments describing the version field
*/
String[] fieldComments() default {
"", /* empty line */
"The version of this configuration - DO NOT CHANGE!"
};
/**
* Returns an {@link UpdateStrategy} describing the actions applied to different
* versions of a configuration file when a version change is detected.
*
* @return {@code UpdateStrategy} applied to a configuration file
*/
UpdateStrategy updateStrategy() default UpdateStrategy.DEFAULT;
}

@ -1,62 +0,0 @@
package de.exlll.configlib;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.BaseConstructor;
import org.yaml.snakeyaml.parser.ParserException;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import java.util.Map;
import java.util.Objects;
final class YamlSerializer {
private final Yaml yaml;
/**
* @param baseConstructor BaseConstructor which is used to configure the {@code Yaml} object.
* @param representer Representer which is used to configure the {@code Yaml} object.
* @param dumperOptions DumperOptions which is used to configure the {@code Yaml} object.
* @param resolver Resolver which is used to configure the {@code Yaml} object.
* @see org.yaml.snakeyaml.constructor.BaseConstructor
* @see org.yaml.snakeyaml.representer.Representer
* @see org.yaml.snakeyaml.DumperOptions
* @see org.yaml.snakeyaml.resolver.Resolver
*/
YamlSerializer(BaseConstructor baseConstructor,
Representer representer,
DumperOptions dumperOptions,
Resolver resolver) {
Objects.requireNonNull(baseConstructor);
Objects.requireNonNull(representer);
Objects.requireNonNull(dumperOptions);
Objects.requireNonNull(resolver);
yaml = new Yaml(baseConstructor, representer, dumperOptions, resolver);
}
/**
* Serializes a Map.
*
* @param map Map to serialize
* @return a serialized representation of the Map
* @throws NullPointerException if {@code map} is null.
*/
String serialize(Map<String, ?> map) {
return yaml.dump(map);
}
/**
* Deserializes a serialized Map.
*
* @param serializedMap a serialized YAML representation of a Map
* @return deserialized Map
* @throws ClassCastException if {@code serializedMap} doesn't represent a Map
* @throws NullPointerException if {@code serializedMap} is null
* @throws ParserException if {@code serializedMap} is invalid YAML
*/
Map<String, Object> deserialize(String serializedMap) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) yaml.load(serializedMap);
return map;
}
}

@ -1,4 +1,4 @@
package de.exlll.configlib;
package de.exlll.configlib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -9,18 +9,22 @@ import java.lang.annotation.Target;
* Indicates that the annotated element is saved together with explanatory
* comments describing it.
* <p>
* When this annotation is used on a class, the comments returned by its
* 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.
* If it's used on a field, the comments are saved above the field name.
* </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.
* <p>
* When the configuration is saved, every comment is written into a new line.
* Empty comments function as newlines.
*
* @return class or field comments
*/

@ -0,0 +1,15 @@
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 {}

@ -0,0 +1,18 @@
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();
}

@ -0,0 +1,16 @@
package de.exlll.configlib.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Indicates the of elements a {@code Collection} or {@code Map} contains.
* <p>
* This annotation must be used if element type is not simple.
*/
@Target(java.lang.annotation.ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ElementType {
Class<?> value();
}

@ -0,0 +1,17 @@
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 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 {}

@ -0,0 +1,46 @@
package de.exlll.configlib.configs.yaml;
import de.exlll.configlib.Comments;
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() {
Map<String, List<String>> fieldComments = comments.getFieldComments();
return fieldComments.entrySet().stream()
.map(this::toStringCommentEntry)
.collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private Map.Entry<String, String> toStringCommentEntry(
Map.Entry<String, List<String>> entry
) {
String fieldComments = commentListToString(entry.getValue());
return Map.entry(entry.getKey(), 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;
}
}

@ -0,0 +1,212 @@
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);
}
}
}
}

@ -0,0 +1,135 @@
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.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 = List.of(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();
for (Map.Entry<String, String> entry : map.entrySet()) {
String prefix = entry.getKey() + ":";
if (dumpLine.startsWith(prefix)) {
builder.append(entry.getValue()).append('\n');
break;
}
}
}
}
}

@ -0,0 +1,13 @@
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);
}
}

@ -0,0 +1,35 @@
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();
}
}
}

@ -1,23 +0,0 @@
import de.exlll.configlib.*;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
ConfigurationTest.class,
ConfigReadWriteTest.class,
CommentAdderTest.class,
CommentsTest.class,
ConfigListTest.class,
ConfigSetTest.class,
ConfigMapTest.class,
FieldFilterTest.class,
FieldMapperTest.class,
FilteredFieldsTest.class,
ReflectTest.class,
TypeConverterTest.class,
VersionTest.class,
YamlSerializerTest.class
})
public class ConfigLibTestSuite {
}

@ -1,26 +0,0 @@
package de.exlll.configlib;
import org.junit.Test;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class CommentAdderTest {
@Test
public void adderAddsComments() throws Exception {
String originalText = "i: 0\n" +
"j: 0\n" +
"k: 0\n";
String commentedText = "# a\n" +
"# b\n\n" +
"# c\n" +
"i: 0\n" +
"# d\n" +
"# e\n" +
"j: 0\n" +
"k: 0\n";
Comments comments = new Comments(CommentsTest.TestClass.class);
CommentAdder adder = new CommentAdder(comments);
assertThat(adder.addComments(originalText), is(commentedText));
}
}

@ -1,31 +1,55 @@
package de.exlll.configlib;
import org.junit.Test;
import de.exlll.configlib.annotation.Comment;
import org.junit.jupiter.api.Test;
import java.util.*;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class CommentsTest {
@Comment({"a", "b"})
public static final class TestClass {
@Comment("c")
private int i;
@Comment({"d", "e"})
private int j;
private int k;
}
@Test
public void getCommentsReturnsComments() throws Exception {
Comments comments = new Comments(TestClass.class);
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());
assertThat(comments.getClassComments(), is(Arrays.asList("a", "b")));
comments = Comments.ofClass(B.class);
assertThat(comments.getClassComments(), is(List.of("B")));
assertThat(comments.getFieldComments().entrySet(), empty());
comments = Comments.ofClass(C.class);
assertThat(comments.getClassComments(), is(List.of("C", "D")));
assertThat(comments.getFieldComments().entrySet(), empty());
}
@Test
void fieldCommentsAdded() {
class A {
int a;
@Comment("b")
int b;
@Comment({"c", "d"})
int c;
}
assertThat(comments.getFieldComments().get("i"), is(Collections.singletonList("c")));
assertThat(comments.getFieldComments().get("j"), is(Arrays.asList("d", "e")));
assertThat(comments.getFieldComments().get("k"), nullValue());
Comments comments = Comments.ofClass(A.class);
assertThat(comments.getClassComments(), empty());
assertThat(comments.getFieldComments(), is(Map.of(
"b", List.of("b"),
"c", List.of("c", "d")
)));
}
}

@ -1,82 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.classes.ConfigTypeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static de.exlll.configlib.classes.ConfigTypeClass.A;
import static de.exlll.configlib.classes.ConfigTypeClass.from;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class ConfigListTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void constructorRequiresNonNullClass() throws Exception {
expectedException.expect(NullPointerException.class);
new ConfigList<>(null);
}
@Test
public void constructorRequiresClassWithDefaultConstructor() throws Exception {
new ConfigList<>(String.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Class ConfigList doesn't have a default constructor.");
new ConfigList<>(ConfigList.class);
}
@Test
public void toDefaultReturnsArrayList() throws Exception {
List<?> list = new ConfigList<>(String.class).toDefault();
assertThat(list, instanceOf(ArrayList.class));
}
@Test
public void toDefaultReturnsSimpleTypes() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
List<?> l = c.configListSimple.toDefault();
assertThat(l, is(c.configListSimple.getList()));
}
@Test
public void toDefaultReturnsSerializedObjects() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
List<?> l = c.configList.toDefault();
for (int i = 0; i < c.configList.size(); i++) {
Object mapped = FieldMapper.instanceToMap(c.configList.get(i));
assertThat(mapped, is(l.get(i)));
}
}
@Test
public void fromDefaultAddsSimpleTypes() throws Exception {
List<String> list = Arrays.asList("a", "b");
ConfigList<String> configList = new ConfigList<>(String.class);
configList.fromDefault(list);
assertThat(configList.getList(), is(list));
}
@Test
public void fromDefaultAddsDeserializedObjects() throws Exception {
A a = from("a");
A b = from("b");
Map<String, ?> map1 = FieldMapper.instanceToMap(a);
Map<String, ?> map2 = FieldMapper.instanceToMap(b);
List<Map<String, ?>> list = Arrays.asList(map1, map2);
ConfigList<A> configList = new ConfigList<>(A.class);
configList.fromDefault(list);
assertThat(configList.getList(), is(Arrays.asList(a, b)));
}
}

@ -1,103 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.classes.ConfigTypeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.*;
import static de.exlll.configlib.classes.ConfigTypeClass.*;
import static de.exlll.configlib.classes.ConfigTypeClass.from;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class ConfigMapTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void constructorRequiresNonNullKeyClass() throws Exception {
expectedException.expect(NullPointerException.class);
new ConfigMap<>(null, Integer.class);
}
@Test
public void constructorRequiresNonNullValueClass() throws Exception {
expectedException.expect(NullPointerException.class);
new ConfigMap<>(String.class, null);
}
@Test
public void constructorRequiresValueClassWithDefaultConstructor() throws Exception {
new ConfigMap<>(String.class, String.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Class ConfigMap doesn't have a default constructor.");
new ConfigMap<>(String.class, ConfigMap.class);
}
@Test
public void constructorRequiresKeyClassWithSimpleType() throws Exception {
new ConfigMap<>(String.class, ConfigListTest.class);
String msg = "Class " + Map.class.getSimpleName() + " is not a simple type.\n" +
"Only simple types can be used as keys in a map.";
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(msg);
new ConfigMap<>(Map.class, ConfigListTest.class);
}
@Test
public void toDefaultReturnsLinkedHashMap() throws Exception {
Map<String, ?> newMap = new ConfigMap<>(String.class, Integer.class).toDefault();
assertThat(newMap, instanceOf(LinkedHashMap.class));
}
@Test
public void toDefaultReturnsSimpleTypes() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
Map<String, ?> m = c.configMapSimple.toDefault();
assertThat(m, is(c.configMapSimple.getMap()));
}
@Test
public void toDefaultReturnsSerializedObjects() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
Map<String, ?> s = c.configMap.toDefault();
for (Map.Entry<String, ConfigTypeClass.A> entry : c.configMap.entrySet()) {
Object mapped = FieldMapper.instanceToMap(entry.getValue());
assertThat(mapped, is(s.get(entry.getKey())));
}
}
@Test
public void fromDefaultAddsSimpleTypes() throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
ConfigMap<String, Integer> configMap = new ConfigMap<>(String.class, Integer.class);
configMap.fromDefault(map);
assertThat(configMap.getMap(), is(map));
}
@Test
public void fromDefaultAddsDeserializedObjects() throws Exception {
A a = from("a");
A b = from("b");
Map<String, ?> map1 = FieldMapper.instanceToMap(a);
Map<String, ?> map2 = FieldMapper.instanceToMap(b);
Map<String, Map<String, ?>> map = new HashMap<>();
map.put("a", map1);
map.put("b", map2);
ConfigMap<String, A> configMap = new ConfigMap<>(String.class, A.class);
configMap.fromDefault(map);
Map<String, A> newMap = new HashMap<>();
newMap.put("a", a);
newMap.put("b", b);
assertThat(configMap.getMap(), is(newMap));
}
}

@ -1,29 +0,0 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.Test;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class ConfigReadWriteTest {
@Test
public void readWrite() throws Exception {
FileSystem fs = Jimfs.newFileSystem();
Path p = fs.getPath("/path/p");
Files.createDirectories(p.getParent());
String text = "Hello\nWorld\n";
ConfigWriter.write(p, text);
String read = ConfigReader.read(p);
assertThat(read, is(text));
fs.close();
}
}

@ -1,78 +0,0 @@
package de.exlll.configlib;
import de.exlll.configlib.classes.ConfigTypeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.*;
import static de.exlll.configlib.classes.ConfigTypeClass.*;
import static de.exlll.configlib.classes.ConfigTypeClass.from;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
public class ConfigSetTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void constructorRequiresNonNullClass() throws Exception {
expectedException.expect(NullPointerException.class);
new ConfigSet<>(null);
}
@Test
public void constructorRequiresClassWithDefaultConstructor() throws Exception {
new ConfigSet<>(String.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Class ConfigSet doesn't have a default constructor.");
new ConfigSet<>(ConfigSet.class);
}
@Test
public void toDefaultReturnsLinkedHashSet() throws Exception {
Set<?> set = new ConfigSet<>(String.class).toDefault();
assertThat(set, instanceOf(LinkedHashSet.class));
}
@Test
public void toDefaultReturnsSimpleTypes() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
Set<?> s = c.configSetSimple.toDefault();
assertThat(s, is(c.configSetSimple.getSet()));
}
@Test
public void toDefaultReturnsSerializedObjects() throws Exception {
ConfigTypeClass c = new ConfigTypeClass();
Set<?> s = c.configSet.toDefault();
for (A a : c.configSet) {
Object mapped = FieldMapper.instanceToMap(a);
assertThat(s, contains(mapped));
}
}
@Test
public void fromDefaultAddsSimpleTypes() throws Exception {
Set<String> set = new HashSet<>(Arrays.asList("a", "b"));
ConfigSet<String> configSet = new ConfigSet<>(String.class);
configSet.fromDefault(set);
assertThat(configSet.getSet(), is(set));
}
@Test
public void fromDefaultAddsDeserializedObjects() throws Exception {
A a = from("a");
A b = from("b");
Map<String, ?> map1 = FieldMapper.instanceToMap(a);
Map<String, ?> map2 = FieldMapper.instanceToMap(b);
Set<Map<String, ?>> set = new HashSet<>(Arrays.asList(map1, map2));
ConfigSet<A> configSet = new ConfigSet<>(A.class);
configSet.fromDefault(set);
assertThat(configSet.getSet(), is(new HashSet<>(Arrays.asList(a, b))));
}
}

@ -1,93 +1,49 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.classes.DefaultTypeClass;
import de.exlll.configlib.classes.NonDefaultTypeClass;
import de.exlll.configlib.classes.SimpleTypesClass;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import de.exlll.configlib.configs.mem.InSharedMemoryConfiguration;
import org.junit.jupiter.api.Test;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.IOException;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class ConfigurationTest {
private FileSystem fileSystem;
private Path configPath;
@Before
public void setUp() throws Exception {
fileSystem = Jimfs.newFileSystem();
configPath = fileSystem.getPath("/a/b/config.yml");
}
@After
public void tearDown() throws Exception {
fileSystem.close();
}
@Test
public void saveCreatesParentDirectories() throws Exception {
TestConfiguration cfg = new TestConfiguration(configPath);
assertThat(Files.exists(configPath.getParent()), is(false));
cfg.save();
assertThat(Files.exists(configPath.getParent()), is(true));
}
@Test
public void saveWritesConfig() throws Exception {
TestConfiguration cfg = new TestConfiguration(configPath);
assertThat(Files.exists(configPath), is(false));
cfg.save();
assertThat(Files.exists(configPath), is(true));
assertThat(ConfigReader.read(configPath), is(TestConfiguration.CONFIG_AS_TEXT));
}
@Test
public void simpleTypesConfigSavesAndLoads() throws Exception {
Configuration cfg = new SimpleTypesClass(configPath);
cfg.save();
cfg.load();
}
@Test
public void defaultTypesConfigSavesAndLoads() throws Exception {
Configuration cfg = new DefaultTypeClass(configPath);
cfg.save();
cfg.load();
class ConfigurationTest {
private static class TestHook extends InSharedMemoryConfiguration {
protected TestHook() { super(Properties.builder().build()); }
}
@Test
public void nonDefaultConfigSavesAndLoads() throws Exception {
Configuration cfg = new NonDefaultTypeClass(configPath);
cfg.save();
cfg.load();
}
void configExecutesPreSaveHook() throws IOException {
class A extends TestHook {
int i = 0;
@Test
public void loadExecutesPostLoadHook() throws Exception {
AtomicInteger integer = new AtomicInteger();
Configuration cfg = new TestConfiguration(configPath, integer::incrementAndGet);
@Override
protected void preSave() { i++; }
}
A save = new A();
save.save();
assertThat(save.i, is(1));
cfg.save();
assertThat(integer.get(), is(0));
cfg.load();
assertThat(integer.get(), is(1));
A load = new A();
load.load();
assertThat(load.i, is(1));
}
@Test
public void loadAndSaveExecutesPostLoadHook1() throws Exception {
AtomicInteger integer = new AtomicInteger();
Configuration cfg = new TestConfiguration(configPath, integer::incrementAndGet);
cfg.loadAndSave();
assertThat(integer.get(), is(1));
void configExecutesPostLoadHook() throws IOException {
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,53 +0,0 @@
package de.exlll.configlib;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class FieldFilterTest {
@Test
public void filterTestsFields() throws Exception {
Predicate<Field> test = FieldFilter.INSTANCE;
Class<?> cls = TestClass.class;
assertThat(cls.getDeclaredFields().length, is(9));
assertThat(test.test(cls.getDeclaredField("a")), is(false));
assertThat(test.test(cls.getDeclaredField("b")), is(false));
assertThat(test.test(cls.getDeclaredField("c")), is(false));
assertThat(test.test(cls.getDeclaredField("d")), is(true));
assertThat(test.test(cls.getDeclaredField("e")), is(true));
assertThat(test.test(cls.getDeclaredField("f")), is(true));
assertThat(test.test(cls.getDeclaredField("g")), is(true));
assertThat(test.test(cls.getDeclaredField("h")), is(true));
assertThat(test.test(cls.getDeclaredField("i")), is(true));
cls = TestClassSynthetic.class;
assertThat(test.test(cls.getDeclaredField("a")), is(true));
assertThat(test.test(cls.getDeclaredField("this$0")), is(false));
}
private static final class TestClass {
/* filtered fields */
private static int a;
private final int b = 1;
private transient int c;
/* not filtered fields */
private int d;
protected int e;
int f;
public int g;
double h;
volatile int i;
}
private final class TestClassSynthetic {
int a;
}
}

@ -0,0 +1,389 @@
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 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 List.of(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 Map.of("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 = Map.of("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(), Map.of("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(List.of(1, 2)));
assertThat(map.get("p2"), is(Map.of("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 = Map.of(
"p1", List.of(10, 11),
"p2", Map.of("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 = Map.of(
"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 = Map.of(
"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 = Map.of(
"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 = Map.of(
"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(LocalTestEnum.class)
List<List<LocalTestEnum>> l = List.of();
}
Map<String, Object> map = Map.of(
"l", List.of(List.of("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(LocalTestEnum.class)
Set<List<LocalTestEnum>> s = Set.of();
}
Map<String, Object> map = Map.of(
"s", Set.of(List.of("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(LocalTestEnum.class)
Map<Integer, List<LocalTestEnum>> m = Map.of();
}
Map<String, Object> map = Map.of(
"m", Map.of(1, List.of("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));
}
}

@ -0,0 +1,121 @@
package de.exlll.configlib;
import de.exlll.configlib.annotation.ConfigurationElement;
import java.util.Map;
import static de.exlll.configlib.Configuration.Properties;
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 assertItmCfgExceptionMessage(
Object o, Properties props, String msg
) {
ConfigurationException ex = assertItmThrowsCfgException(o, props);
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 void assertIfmCfgExceptionMessage(
Object o, Map<String, Object> map, Properties props, String msg
) {
ConfigurationException ex = assertIfmThrowsCfgException(o, map, props);
assertThat(ex.getMessage(), is(msg));
}
public static ConfigurationException assertItmThrowsCfgException(Object o) {
return assertThrows(
ConfigurationException.class,
() -> instanceToMap(o)
);
}
public static ConfigurationException assertItmThrowsCfgException(
Object o, Configuration.Properties props
) {
return assertThrows(
ConfigurationException.class,
() -> instanceToMap(o, props)
);
}
public static ConfigurationException assertIfmThrowsCfgException(
Object o, Map<String, Object> map
) {
return assertThrows(
ConfigurationException.class,
() -> instanceFromMap(o, map)
);
}
public static ConfigurationException assertIfmThrowsCfgException(
Object o, Map<String, Object> map, Configuration.Properties props
) {
return assertThrows(
ConfigurationException.class,
() -> instanceFromMap(o, map, props)
);
}
public static Map<String, Object> instanceToMap(Object o) {
return instanceToMap(o, DEFAULT);
}
public static Map<String, Object> instanceToMap(
Object o, Configuration.Properties props) {
return FieldMapper.instanceToMap(o, props);
}
public static <T> T instanceFromMap(T o, Map<String, Object> map) {
FieldMapper.instanceFromMap(o, map, DEFAULT);
return o;
}
public static <T> T instanceFromMap(
T o, Map<String, Object> map, Configuration.Properties props) {
FieldMapper.instanceFromMap(o, map, props);
return o;
}
}

@ -1,197 +1,530 @@
package de.exlll.configlib;
import de.exlll.configlib.classes.DefaultTypeClass;
import de.exlll.configlib.classes.NonDefaultTypeClass;
import org.junit.Test;
import de.exlll.configlib.Converter.ConversionInfo;
import de.exlll.configlib.FieldMapper.FieldFilter;
import de.exlll.configlib.annotation.ElementType;
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.format.FieldNameFormatters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
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 java.util.stream.Collectors.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SuppressWarnings({"unused", "ThrowableNotThrown"})
class FieldMapperTest {
private static final Class<ClassWithFinalStaticTransientField> CWFSTF =
ClassWithFinalStaticTransientField.class;
private static final Predicate<Field> filter = FieldFilter.DEFAULT;
private static final TestClass t = TestClass.TEST_VALUES;
private static final Configuration.Properties DEFAULT =
Configuration.Properties.builder().build();
private static final Map<String, Object> map = instanceToMap(
TestClass.TEST_VALUES, DEFAULT
);
private TestClass tmp;
@BeforeEach
void setUp() {
tmp = new TestClass();
FieldMapper.instanceFromMap(tmp, map, DEFAULT);
}
public class FieldMapperTest {
private final Path path = Paths.get("a");
@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
public void toSerializableObjectReturnsObjectForDefaultTypes() throws Exception {
DefaultTypeClass instance = new DefaultTypeClass(path);
for (Field f : DefaultTypeClass.class.getDeclaredFields()) {
Object value = Reflect.getValue(f, instance);
assertThat(FieldMapper.toSerializableObject(value), sameInstance(value));
}
void instanceFromMapSetsEnums() {
assertThat(tmp.getE1(), is(t.getE1()));
}
@Test
public void toSerializableObjectReturnsMapForNonDefaultTypes() throws Exception {
DefaultTypeClass instance = new DefaultTypeClass(path);
void instanceFromMapSetsContainersOfSimpleTypes() {
assertAll(
() -> assertThat(tmp.getInts(), is(t.getInts())),
() -> assertThat(tmp.getStrings(), is(t.getStrings())),
() -> assertThat(tmp.getDoubleByBool(), is(t.getDoubleByBool()))
);
}
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) FieldMapper.toSerializableObject(instance);
@Test
void instanceFromMapsConvertsMapsToTypes() {
assertThat(tmp.getSubClass(), is(t.getSubClass()));
}
int counter = 0;
for (Field field : DefaultTypeClass.class.getDeclaredFields()) {
Object fieldValue = Reflect.getValue(field, instance);
assertThat(map.get(field.getName()), is(fieldValue));
counter++;
}
assertThat(map.size(), is(counter));
@Test
void instanceFromMapsConvertsExcludedClasses() {
assertThat(tmp.getExcludedClass(), is(t.getExcludedClass()));
}
@Test
public void fromSerializedObjectIgnoresNullValues() throws Exception {
DefaultTypeClass instance = new DefaultTypeClass(path);
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()));
}
for (Field field : DefaultTypeClass.class.getDeclaredFields()) {
Object currentValue = Reflect.getValue(field, instance);
FieldMapper.fromSerializedObject(field, instance, null);
Object newValue = Reflect.getValue(field, instance);
@Test
void instanceFromMapDoesNotSetFinalStaticOrTransientFields() {
Map<String, Object> map = Map.of(
"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));
}
if (field.getType().isPrimitive()) {
assertThat(currentValue, is(newValue));
} else {
assertThat(currentValue, sameInstance(newValue));
}
@Test
void instanceFromMapConvertsAllFields() {
assertThat(tmp, is(t));
}
@Test
void instanceFromMapThrowsExceptionIfEnumConstantDoesNotExist() {
class A {
LocalTestEnum t = LocalTestEnum.T;
}
Map<String, Object> map = Map.of(
"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
public void fromSerializedObjectSetsValueIfDefaultType() throws Exception {
DefaultTypeClass instance = new DefaultTypeClass(path);
void instanceFromMapIgnoresNullValues() {
class A {
TestSubClass c = new TestSubClass();
}
Map<String, Object> map = Map.of("c", Map.of("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(""));
}
Map<String, Object> map = DefaultTypeClass.newValues();
for (Field field : DefaultTypeClass.class.getDeclaredFields()) {
String fieldName = field.getName();
Object mapValue = map.get(fieldName);
FieldMapper.fromSerializedObject(field, instance, mapValue);
Object value = Reflect.getValue(field, instance);
@Test
void instanceFromMapSetsField() {
TestClass ins = TestClass.TEST_VALUES;
TestClass def = new TestClass();
assertThat(ins, is(not(def)));
instanceFromMap(def, map);
assertThat(ins, is(def));
}
if (field.getType().isPrimitive()) {
assertThat(mapValue, is(value));
} else {
assertThat(mapValue, sameInstance(value));
}
@Test
void instanceFromMapCreatesConcreteInstances() {
class A {
LocalTestInterface l = new LocalTestInterfaceImpl();
}
class B {
LocalTestAbstractClass l = new LocalTestAbstractClassImpl();
}
instanceFromMap(new A(), Map.of("l", Map.of()));
instanceFromMap(new B(), Map.of("l", Map.of()));
}
@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
public void fromSerializedObjectUpdatesValueIfNotDefaultType() throws Exception {
NonDefaultTypeClass instance = new NonDefaultTypeClass(path);
Field field = NonDefaultTypeClass.class.getDeclaredField("defaultTypeClass");
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()))
);
}
Map<String, Object> map = DefaultTypeClass.newValues();
FieldMapper.fromSerializedObject(field, instance, map);
@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 = List.of(LocalTestEnum.S, LocalTestEnum.T);
@ElementType(LocalTestEnum.class)
Set<LocalTestEnum> es = Set.of(LocalTestEnum.S, LocalTestEnum.T);
@ElementType(LocalTestEnum.class)
Map<String, LocalTestEnum> em = Map.of(
"1", LocalTestEnum.S, "2", LocalTestEnum.T
);
}
Map<String, Object> map = instanceToMap(new A());
assertThat(map.get("el"), is(List.of("S", "T")));
assertThat(map.get("es"), is(Set.of("S", "T")));
assertThat(map.get("em"), is(Map.of("1", "S", "2", "T")));
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
Field f = DefaultTypeClass.class.getDeclaredField(entry.getKey());
Object value = Reflect.getValue(f, instance.defaultTypeClass);
assertThat(value, is(entry.getValue()));
@Test
void instanceFromMapConvertsStringContainersToEnumContainers() {
class A {
@ElementType(LocalTestEnum.class)
List<LocalTestEnum> el = List.of();
@ElementType(LocalTestEnum.class)
Set<LocalTestEnum> es = Set.of();
@ElementType(LocalTestEnum.class)
Map<String, LocalTestEnum> em = Map.of();
}
Map<String, Object> map = Map.of(
"el", List.of("S", "T"),
"es", Set.of("S", "T"),
"em", Map.of("1", "S", "2", "T")
);
A a = instanceFromMap(new A(), map);
assertThat(a.el, is(List.of(LocalTestEnum.S, LocalTestEnum.T)));
assertThat(a.es, is(Set.of(LocalTestEnum.S, LocalTestEnum.T)));
assertThat(a.em, is(Map.of(
"1", LocalTestEnum.S, "2", LocalTestEnum.T
)));
}
@Test
public void instanceTopMapCreatesMap() throws Exception {
TestClass t = new TestClass();
Map<String, Object> map = FieldMapper.instanceToMap(t);
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 -> Map.entry(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))
);
}
assertThat(map.get("i"), is(1));
assertThat(map.get("i"), instanceOf(Integer.class));
@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()))
);
}
assertThat(map.get("z"), is(0));
assertThat(map.get("z"), instanceOf(Integer.class));
@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 -> Map.entry(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));
}
assertThat(map.get("d"), is(2.0));
assertThat(map.get("d"), instanceOf(Double.class));
@Test
void instanceToMapContainsEnums() {
assertThat(map.get("e1"), is(t.getE1().toString()));
}
assertThat(map.get("s"), is("s"));
assertThat(map.get("s"), instanceOf(String.class));
@Test
void instanceToMapContainsEnumLists() {
List<String> list = t.getEnums().stream()
.map(Enum::toString)
.collect(toList());
assertThat(map.get("enums"), is(list));
}
assertThat(map.get("c"), is('c'));
assertThat(map.get("c"), instanceOf(Character.class));
assertThat(map.get("strings"), is(Arrays.asList("1", "2")));
assertThat(map.get("strings"), instanceOf(List.class));
@Test
void instanceToMapConvertsConvertTypes() {
String s = new TestSubClassConverter()
.convertTo(t.getConverterSubClass(), null);
assertThat(map.get("converterSubClass"), is(s));
}
Map<String, Integer> intMap = new HashMap<>();
intMap.put("a", 1);
intMap.put("b", 2);
assertThat(map.get("objects"), is(intMap));
assertThat(map.get("objects"), instanceOf(Map.class));
@Test
void instanceToMapCreatesLinkedHashMap() {
assertThat(instanceToMap(new Object()), instanceOf(LinkedHashMap.class));
}
Map<String, Object> bMap = new HashMap<>();
bMap.put("j", -1);
bMap.put("t", "t");
assertThat(map.get("b"), is(bMap));
assertThat(map.get("b"), instanceOf(Map.class));
@Test
void filteredFieldsFiltersFields() throws NoSuchFieldException {
List<Field> fields = FieldFilter.filterFields(CWFSTF);
assertThat(fields.size(), is(0));
class A {
private int i;
private final int j = 0;
private transient int k;
}
fields = FieldFilter.filterFields(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
public void instanceFromMapKeepsDefaultValues() throws Exception {
TestClass t = new TestClass();
FieldMapper.instanceFromMap(t, new HashMap<>());
assertThat(t.z, is(0));
assertThat(t.i, is(1));
assertThat(t.s, is("s"));
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 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();
}
return ConversionInfo.of(field, o, null, null);
}
private static ConversionInfo newInfo(String fieldName) {
return newInfo(fieldName, new TestClass());
}
@Test
public void instanceFromMapSetsValues() throws Exception {
TestClass t = new TestClass();
void typeConverterReturnsInstanceIfClassesMatch() {
//noinspection RedundantStringConstructorCall
String s = new String("123");
Object val = converter.convertFrom(s, newInfo("string"));
assertThat(val, sameInstance(s));
}
Map<String, Object> map = new HashMap<>();
map.put("z", 2);
map.put("i", 10);
map.put("c", 'q');
map.put("s", "t");
map.put("strings", Arrays.asList("99", "100", "101"));
@Test
void typeConverterConvertsStringToCharacter() {
String s = "1";
Object vc = converter.convertFrom(s, newInfo("primChar"));
Object vd = converter.convertFrom(s, newInfo("refChar"));
Map<String, Object> objects = new HashMap<>();
objects.put("a", 100);
objects.put("b", 200);
objects.put("c", 300);
objects.put("d", 400);
map.put("objects", objects);
assertThat(vc, instanceOf(Character.class));
assertThat(vd, instanceOf(Character.class));
}
Map<String, Object> bMap = new HashMap<>();
bMap.put("j", 20);
bMap.put("t", "v");
map.put("b", bMap);
@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 conv = converter.convertFrom(number, info);
assertThat(conv, instanceOf(info.getFieldType()));
}
}
}
FieldMapper.instanceFromMap(t, map);
assertThat(t.z, is(2));
assertThat(t.i, is(10));
assertThat(t.c, is('q'));
assertThat(t.s, is("t"));
assertThat(t.strings, is(Arrays.asList("99", "100", "101")));
assertThat(t.objects, is(objects));
assertThat(t.b.j, is(20));
assertThat(t.b.t, is("v"));
@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 TestClass {
private int z;
private int i = 1;
private double d = 2.0;
private String s = "s";
private List<String> strings = Arrays.asList("1", "2");
private Map<String, Object> objects = new HashMap<>();
private char c = 'c';
private TestClassB b = new TestClassB();
private static final class ExcludedClass {}
public TestClass() {
objects.put("a", 1);
objects.put("b", 2);
@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));
}
private static final class TestClassB {
private int j = -1;
private String t = "t";
@Test
void instanceFromMapSetsAutoConvertedInstancesAsIs() {
class A {
@NoConvert
ExcludedClass ex = new ExcludedClass();
}
ExcludedClass cls = new ExcludedClass();
Map<String, Object> map = Map.of("ex", cls);
A a = instanceFromMap(new A(), map);
assertThat(a.ex, sameInstance(cls));
}
private static final class ClassWithFinalStaticTransientField {
private final int i = 0;
private static int j;
private transient int k;
}
private final class ClassWithSyntheticField {}
}

@ -0,0 +1,24 @@
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"));
}
}

@ -1,51 +0,0 @@
package de.exlll.configlib;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class FilteredFieldsTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void constructorRequiresNonNullArray() throws Exception {
expectedException.expect(NullPointerException.class);
new FilteredFields(null, field -> true);
}
@Test
public void constructorRequiresNonNullPredicate() throws Exception {
expectedException.expect(NullPointerException.class);
new FilteredFields(FilteredFields.class.getDeclaredFields(), null);
}
@Test
public void constructorAppliesFilter() throws Exception {
FilteredFields ff = new FilteredFields(
TestClass.class.getDeclaredFields(), field -> {
int mods = field.getModifiers();
return Modifier.isPublic(mods);
});
for (Field f : ff) {
int mods = f.getModifiers();
assertThat(Modifier.isPublic(mods), is(true));
}
}
private static final class TestClass {
private int a;
protected int b;
int c;
public int d;
}
}

@ -1,160 +1,60 @@
package de.exlll.configlib;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
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.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class ReflectTest {
private static final Class<TestClass> cls1 = TestClass.class;
private static final Class<NotDefaultConstructor> cls2 = NotDefaultConstructor.class;
private final List<String> list = new ConfigList<>(String.class);
private final Set<String> set = new ConfigSet<>(String.class);
private final Map<?, String> map = new ConfigMap<>(String.class, String.class);
private final Class<?>[] containerClasses = {List.class, Set.class, Map.class};
private final Class<?>[] simpleClasses = {
boolean.class, char.class, byte.class, short.class,
int.class, long.class, float.class, double.class,
Boolean.class, String.class, Character.class,
Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class,
};
private final String errorMessage = "Class NotDefaultConstructor doesn't have a default constructor.";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void checkType() throws Exception {
Reflect.checkType(new HashMap<>(), Map.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Invalid type!\n" +
"Object 'a' is of type String. Expected type: Map");
Reflect.checkType("a", Map.class);
}
@Test
public void checkMapEntriesChecksKeys() throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
Reflect.checkMapEntries(map, String.class, Integer.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Invalid type!\n" +
"Object 'a' is of type String. Expected type: Integer");
Reflect.checkMapEntries(map, Integer.class, Integer.class);
}
@Test
public void checkMapEntriesChecksValues() throws Exception {
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
Reflect.checkMapEntries(map, String.class, Integer.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage("Invalid type!\n" +
"Object '1' is of type Integer. Expected type: String");
Reflect.checkMapEntries(map, String.class, String.class);
}
@Test
public void isDefault() throws Exception {
for (Class<?> cls : containerClasses) {
assertThat(Reflect.isDefault(cls), is(true));
}
for (Class<?> cls : simpleClasses) {
assertThat(Reflect.isDefault(cls), is(true));
}
}
@Test
public void isSimpleType() throws Exception {
for (Class<?> cls : simpleClasses) {
class ReflectTest {
private static final Set<Class<?>> ALL_SIMPLE_TYPES = Set.of(
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
public void isContainerType() throws Exception {
assertThat(Reflect.isContainerType(list.getClass()), is(true));
assertThat(Reflect.isContainerType(set.getClass()), is(true));
assertThat(Reflect.isContainerType(map.getClass()), is(true));
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
public void getValueGetsValue() throws Exception {
TestClass testClass = new TestClass();
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");
Field s = TestClass.class.getDeclaredField("s");
assertThat(Reflect.getValue(s, testClass), is("s"));
}
@Test
public void setValueSetsValue() throws Exception {
TestClass testClass = new TestClass();
Field s = TestClass.class.getDeclaredField("s");
Reflect.setValue(s, testClass, "t");
assertThat(testClass.s, is("t"));
}
Object value = Reflect.getValue(f1, testClass);
assertThat(value, is(testClass.getString()));
@Test
public void checkForDefaultConstructorsThrowsExceptionIfNoDefault() throws Exception {
Reflect.checkDefaultConstructor(cls1);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(errorMessage);
Reflect.checkDefaultConstructor(cls2);
}
@Test
public void hasDefaultConstructor() throws Exception {
assertThat(Reflect.hasDefaultConstructor(cls1), is(true));
assertThat(Reflect.hasDefaultConstructor(cls2), is(false));
}
@Test
public void getDefaultConstructor() throws Exception {
assertThat(Reflect.getDefaultConstructor(cls1), is(cls1.getDeclaredConstructor()));
expectedException.expect(RuntimeException.class);
Reflect.getDefaultConstructor(cls2);
}
@Test
public void newInstanceChecksForDefaultConstructor() throws Exception {
Reflect.newInstance(TestClass.class);
expectedException.expect(IllegalArgumentException.class);
expectedException.expectMessage(errorMessage);
Reflect.newInstance(NotDefaultConstructor.class);
}
@Test
public void newInstanceCreatesNewInstance() throws Exception {
TestClass t = (TestClass) Reflect.newInstance(TestClass.class);
}
@Test
public void getVersion() throws Exception {
assertThat(Reflect.getVersion(cls1), notNullValue());
assertThat(Reflect.getVersion(cls2), nullValue());
}
private static final class NotDefaultConstructor {
public NotDefaultConstructor(String a) {
}
}
value = Reflect.getValue(f2, testClass);
assertThat(value, is(testClass.getPrimLong()));
@Version(version = "1.2.3")
private static final class TestClass {
private String s = "s";
value = Reflect.getValue(f3, testClass);
assertThat(value, is(TestClass.getStaticFinalInt()));
}
}

@ -1,145 +0,0 @@
package de.exlll.configlib;
import java.nio.file.Path;
import java.util.*;
@Comment({
"This is a test configuration.",
"This comment is applied to a class."
})
final class TestConfiguration extends Configuration {
private transient Runnable postLoadAction;
@Comment({
"This comment is applied to a field.",
"It has more than 1 line."
})
private int port = -1;
private String localhost = "localhost";
private double modifier = 3.14;
@Comment("This comment is applied to a field.")
private List<String> allowedIps = new ArrayList<>();
private Map<String, Integer> intsByStrings = new HashMap<>();
private Map<String, List<String>> stringListsByString = new HashMap<>();
private Credentials credentials = new Credentials();
public TestConfiguration(Path path) {
super(path);
allowedIps.add("127.0.0.1");
allowedIps.add("127.0.0.2");
allowedIps.add("127.0.0.3");
intsByStrings.put("first", 1);
intsByStrings.put("second", 2);
intsByStrings.put("third", 3);
stringListsByString.put("xa", Arrays.asList("x1", "x2"));
stringListsByString.put("ya", Arrays.asList("y1", "y2"));
stringListsByString.put("za", Arrays.asList("z1", "z2"));
}
public TestConfiguration(Path path, Runnable postLoadAction) {
this(path);
this.postLoadAction = postLoadAction;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getLocalhost() {
return localhost;
}
public void setLocalhost(String localhost) {
this.localhost = localhost;
}
public double getModifier() {
return modifier;
}
public void setModifier(double modifier) {
this.modifier = modifier;
}
public List<String> getAllowedIps() {
return allowedIps;
}
public void setAllowedIps(List<String> allowedIps) {
this.allowedIps = allowedIps;
}
public Map<String, Integer> getIntsByStrings() {
return intsByStrings;
}
public void setIntsByStrings(Map<String, Integer> intsByStrings) {
this.intsByStrings = intsByStrings;
}
public Map<String, List<String>> getStringListsByString() {
return stringListsByString;
}
public void setStringListsByString(
Map<String, List<String>> stringListsByString) {
this.stringListsByString = stringListsByString;
}
public Credentials getCredentials() {
return credentials;
}
public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}
public static final class Credentials {
private String username = "root";
private String password = "1234";
}
public static final String CONFIG_AS_TEXT = "# This is a test configuration.\n" +
"# This comment is applied to a class.\n" +
"\n" +
"# This comment is applied to a field.\n" +
"# It has more than 1 line.\n" +
"port: -1\n" +
"localhost: localhost\n" +
"modifier: 3.14\n" +
"# This comment is applied to a field.\n" +
"allowedIps:\n" +
"- 127.0.0.1\n" +
"- 127.0.0.2\n" +
"- 127.0.0.3\n" +
"intsByStrings:\n" +
" third: 3\n" +
" first: 1\n" +
" second: 2\n" +
"stringListsByString:\n" +
" za:\n" +
" - z1\n" +
" - z2\n" +
" ya:\n" +
" - y1\n" +
" - y2\n" +
" xa:\n" +
" - x1\n" +
" - x2\n" +
"credentials:\n" +
" username: root\n" +
" password: '1234'\n";
@Override
protected void postLoadHook() {
if (postLoadAction != null) {
postLoadAction.run();
}
}
}

@ -1,167 +0,0 @@
package de.exlll.configlib;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
public class TypeConverterTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void convertValueReturnsSameInstanceIfNoConversion() throws Exception {
Map<?, ?> map = new HashMap<>();
assertThat(TypeConverter.convertValue(Map.class, map), sameInstance(map));
}
@Test
public void convertValueConvertsString() throws Exception {
assertThat(TypeConverter.convertString("z"), is('z'));
}
@Test
public void convertValueConvertsNumber() throws Exception {
Integer i = 10;
Object o = TypeConverter.convertValue(Short.class, i);
assertThat(o, instanceOf(Short.class));
assertThat(o, is((short) 10));
}
@Test
public void convertValueReturnsInstanceIfTypesMatch() throws Exception {
Object o = new Object();
Object converted = TypeConverter.convertValue(Object.class, o);
assertThat(o, sameInstance(converted));
}
@Test
public void convertValueRequiresNonNullClass() throws Exception {
exception.expect(NullPointerException.class);
TypeConverter.convertValue(Object.class, null);
}
@Test
public void convertValueRequiresNonNullValue() throws Exception {
exception.expect(NullPointerException.class);
TypeConverter.convertValue(null, new Object());
}
@Test
public void convertStringThrowsExceptionIfStringLength0() throws Exception {
exception.expect(IllegalArgumentException.class);
TypeConverter.convertString("");
}
@Test
public void convertStringThrowsExceptionIfStringLengthBiggerThan1() throws Exception {
exception.expect(IllegalArgumentException.class);
TypeConverter.convertString("ab");
}
@Test
public void convertStringReturnsCharacter() throws Exception {
String s = "z";
char c = 'z';
Character character = TypeConverter.convertString(s);
assertThat(character, is(c));
}
@Test
public void convertNumberThrowsExceptionIfUnknownType() throws Exception {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Number cannot be converted to target type " +
"'class java.lang.Object'");
TypeConverter.convertNumber(Object.class, 1);
}
@Test
public void convertNumberReturnsConvertedNumber() throws Exception {
Number[] numbers = {(byte) 1, (short) 2, 3, 4L, 5.0F, 6.0};
Class<?>[] numClasses = {
Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class
};
for (Class<?> numClass : numClasses) {
for (Number number : numbers) {
Number n = TypeConverter.convertNumber(numClass, number);
assertThat(n, instanceOf(numClass));
// this is only true because we use values that
// don't cause an overflow
assertThat(n.doubleValue(), is(number.doubleValue()));
}
}
int i = Short.MAX_VALUE + 1;
Number n = TypeConverter.convertNumber(Short.class, i);
assertThat(n, is(Short.MIN_VALUE));
}
@Test
public void isBooleanClass() throws Exception {
assertThat(TypeConverter.isBooleanClass(Boolean.class), is(true));
assertThat(TypeConverter.isBooleanClass(boolean.class), is(true));
assertThat(TypeConverter.isBooleanClass(Object.class), is(false));
}
@Test
public void isByteClass() throws Exception {
assertThat(TypeConverter.isByteClass(Byte.class), is(true));
assertThat(TypeConverter.isByteClass(byte.class), is(true));
assertThat(TypeConverter.isByteClass(Object.class), is(false));
}
@Test
public void isShortClass() throws Exception {
assertThat(TypeConverter.isShortClass(Short.class), is(true));
assertThat(TypeConverter.isShortClass(short.class), is(true));
assertThat(TypeConverter.isShortClass(Object.class), is(false));
}
@Test
public void isIntegerClass() throws Exception {
assertThat(TypeConverter.isIntegerClass(Integer.class), is(true));
assertThat(TypeConverter.isIntegerClass(int.class), is(true));
assertThat(TypeConverter.isIntegerClass(Object.class), is(false));
}
@Test
public void isLongClass() throws Exception {
assertThat(TypeConverter.isLongClass(Long.class), is(true));
assertThat(TypeConverter.isLongClass(long.class), is(true));
assertThat(TypeConverter.isLongClass(Object.class), is(false));
}
@Test
public void isFloatClass() throws Exception {
assertThat(TypeConverter.isFloatClass(Float.class), is(true));
assertThat(TypeConverter.isFloatClass(float.class), is(true));
assertThat(TypeConverter.isFloatClass(Object.class), is(false));
}
@Test
public void isDoubleClass() throws Exception {
assertThat(TypeConverter.isDoubleClass(Double.class), is(true));
assertThat(TypeConverter.isDoubleClass(double.class), is(true));
assertThat(TypeConverter.isDoubleClass(Object.class), is(false));
}
@Test
public void isCharacterClass() throws Exception {
assertThat(TypeConverter.isCharacterClass(Character.class), is(true));
assertThat(TypeConverter.isCharacterClass(char.class), is(true));
assertThat(TypeConverter.isCharacterClass(Object.class), is(false));
}
}

@ -0,0 +1,532 @@
package de.exlll.configlib;
import de.exlll.configlib.FieldMapperHelpers.*;
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 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 = Map.of(
"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 = Map.of(
"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 = Map.of(
"c", Map.of(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 = Map.of("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 = Map.of("s", Map.of());
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 = Map.of("s", Map.of());
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 = Map.of("l", List.of("s"));
String msg = "Can not set field 'l' with type 'CopyOnWriteArrayList' " +
"to 'List1'.";
assertIfmCfgExceptionMessage(new A(), m, msg);
m = Map.of("s", Set.of("s"));
msg = "Can not set field 's' with type 'ConcurrentSkipListSet' " +
"to 'Set1'.";
assertIfmCfgExceptionMessage(new B(), m, msg);
m = Map.of("m", Map.of(1, "s"));
msg = "Can not set field 'm' with type 'ConcurrentHashMap' " +
"to 'Map1'.";
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 = Map.of("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<>(List.of(
TestSubClass.of(1, "1")
));
}
class B {
List<Set<Map<Integer, TestSubClass>>> l = new ArrayList<>(List.of(
Set.of(Map.of(1, TestSubClass.of(1, "1")))
));
}
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: [TestSubClass{\nprimInt=1,\nstring='1'}]";
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=TestSubClass{\nprimInt=1,\nstring='1'}}]]";
assertItmCfgExceptionMessage(b, msg);
}
@Test
void instanceToMapRequiresSetsWithoutElementTypeToContainSimpleTypes() {
class A {
Set<TestSubClass> s = new HashSet<>(Set.of(
TestSubClass.of(1, "1")
));
}
class B {
Set<List<Map<Integer, TestSubClass>>> s = new HashSet<>(Set.of(
List.of(Map.of(1, TestSubClass.of(1, "1")))
));
}
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: [TestSubClass{\nprimInt=1,\nstring='1'}]";
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=TestSubClass{\nprimInt=1,\nstring='1'}}]]";
assertItmCfgExceptionMessage(b, msg);
}
@Test
void instanceToMapRequiresMapsWithoutElementTypeToContainSimpleTypes() {
class A {
Map<Integer, TestSubClass> m = new HashMap<>(Map.of(
1, TestSubClass.of(1, "1")
));
}
class B {
Map<Integer, Set<List<TestSubClass>>> m = new HashMap<>(Map.of(
1, Set.of(List.of(TestSubClass.of(1, "1")))
));
}
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=TestSubClass{\nprimInt=1,\nstring='1'}}";
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=[[TestSubClass{\nprimInt=1,\nstring='1'}]]}";
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 = Map.of("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 = Map.of("l", List.of());
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(), Map.of("s", ""), msg);
}
@Test
void instanceFromMapsRequiresElementTypeToBeEnumType() {
class A {
@ElementType(TestSubClass.class)
List<List<TestSubClass>> l = List.of();
}
Map<String, Object> map = Map.of(
"l", List.of(List.of("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(TestSubClass.class)
List<List<TestSubClass>> l = List.of();
}
Map<String, Object> map = Map.of(
"l", List.of(List.of(1, 2))
);
ConfigurationException ex = assertIfmThrowsCfgException(new A(), map);
Throwable cause = ex.getCause();
String msg = "Initializing field 'l' requires objects of type " +
"Map<String, Object> but element '1' is of type 'Integer'.";
assertThat(cause.getMessage(), is(msg));
}
}

@ -1,118 +0,0 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class VersionTest {
private static final String CONFIG_AS_STRING =
"# class\n\n# field\nk: 0\n\n# c1\n";
private static final String VERSION_CONFIG_AS_STRING =
"# class\n\n# field\nk: 0\n\n# c1\nv: 1.2.3-alpha\n";
private static final String OLD_VERSION_CONFIG_AS_STRING =
"# class\n\n# field\nk: 0\n\n# c1\nv: 1.2.2-alpha\n";
@Rule
public ExpectedException exception = ExpectedException.none();
private FileSystem fileSystem;
private Path configPath;
@Before
public void setUp() throws Exception {
fileSystem = Jimfs.newFileSystem();
configPath = fileSystem.getPath("/a/b/config.yml");
}
@After
public void tearDown() throws Exception {
fileSystem.close();
}
@Test
public void versionSaved() throws Exception {
new VersionConfiguration(configPath).save();
assertThat(ConfigReader.read(configPath), is(VERSION_CONFIG_AS_STRING));
}
@Test
public void saveThrowsExceptionIfVersionNameClash() throws Exception {
exception.expect(ConfigException.class);
exception.expectMessage("Solution: Rename the field or use a " +
"different version field name.");
new VersionConfigurationWithVersionField(configPath).save();
}
@Test
public void currentFileVersionReturnsEmptyOptionalIfFileDoesntExist() throws Exception {
Configuration cfg = new VersionConfiguration(configPath);
assertThat(cfg.currentFileVersion(), nullValue());
}
@Test
public void currentFileVersionReturnsOptionalWithVersion() throws Exception {
Configuration cfg = new VersionConfiguration(configPath);
cfg.save();
assertThat(cfg.currentFileVersion(), is("1.2.3-alpha"));
}
@Test
public void updateRenameApplied() throws Exception {
Files.createDirectories(configPath.getParent());
ConfigWriter.write(configPath, CONFIG_AS_STRING);
final Path oldPath = fileSystem.getPath(configPath.toString() + "-old");
new VersionConfiguration(configPath).save();
assertThat(ConfigReader.read(configPath), is(VERSION_CONFIG_AS_STRING));
assertThat(ConfigReader.read(oldPath), is(CONFIG_AS_STRING));
}
@Test
public void updateRenameAppendsOldVersion() throws Exception {
final Path oldPath = fileSystem.getPath(configPath.toString() + "-v1.2.2-alpha");
Files.createDirectories(configPath.getParent());
ConfigWriter.write(configPath, OLD_VERSION_CONFIG_AS_STRING);
ConfigWriter.write(oldPath, "123");
new VersionConfiguration(configPath).save();
assertThat(ConfigReader.read(configPath), is(VERSION_CONFIG_AS_STRING));
assertThat(ConfigReader.read(oldPath), is(OLD_VERSION_CONFIG_AS_STRING));
}
@Comment("class")
@Version(version = "1.2.3-alpha",
fieldName = "v",
fieldComments = {"", "c1"},
updateStrategy = UpdateStrategy.DEFAULT_RENAME)
private static final class VersionConfiguration extends Configuration {
@Comment("field")
private int k;
protected VersionConfiguration(Path configPath) {
super(configPath);
}
}
@Version
private static final class VersionConfigurationWithVersionField
extends Configuration {
private int version;
protected VersionConfigurationWithVersionField(Path configPath) {
super(configPath);
}
}
}

@ -1,110 +0,0 @@
package de.exlll.configlib;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
public class YamlSerializerTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
private DumperOptions dumperOptions;
private Map<String, Object> map;
private String serializedMap;
@Before
public void setUp() throws Exception {
dumperOptions = new DumperOptions();
dumperOptions.setIndent(2);
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
map = new HashMap<>();
map.put("1", 1);
map.put("2", 2.2);
map.put("3", "3");
map.put("4", new ArrayList<>());
map.put("5", Arrays.asList("5", "5", "5"));
map.put("6", new HashMap<>());
Map<String, Object> subMap = new HashMap<>();
subMap.put("1", 1);
subMap.put("2", 2.2);
subMap.put("3", "3");
subMap.put("4", new ArrayList<>());
subMap.put("5", Arrays.asList("5", "5", "5"));
subMap.put("6", new HashMap<>());
map.put("7", subMap);
serializedMap = "'1': 1\n" +
"'2': 2.2\n" +
"'3': '3'\n" +
"'4': []\n" +
"'5':\n" +
"- '5'\n" +
"- '5'\n" +
"- '5'\n" +
"'6': {}\n" +
"'7':\n" +
" '1': 1\n" +
" '2': 2.2\n" +
" '3': '3'\n" +
" '4': []\n" +
" '5':\n" +
" - '5'\n" +
" - '5'\n" +
" - '5'\n" +
" '6': {}\n";
}
@Test
public void constructorRequiresNonNullBaseConstructor() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(null, new Representer(), new DumperOptions(), new Resolver());
}
@Test
public void constructorRequiresNonNullRepresenter() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), new Representer(), null, new Resolver());
}
@Test
public void constructorRequiresNonNullDumperOptions() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), null, new DumperOptions(), new Resolver());
}
@Test
public void constructorRequiresNonNullResolver() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), new Representer(), new DumperOptions(), null);
}
@Test
public void serialize() throws Exception {
String s = new YamlSerializer(
new Constructor(), new Representer(), dumperOptions, new Resolver()
).serialize(map);
assertThat(s, is(serializedMap));
}
@Test
public void deserialize() throws Exception {
assertThat(new YamlSerializer(
new Constructor(), new Representer(), dumperOptions, new Resolver()
).deserialize(serializedMap), is(map));
}
}

@ -1,87 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.ConfigList;
import de.exlll.configlib.ConfigMap;
import de.exlll.configlib.ConfigSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ConfigTypeClass {
public ConfigList<String> configListSimple = new ConfigList<>(String.class);
public ConfigSet<String> configSetSimple = new ConfigSet<>(String.class);
public ConfigMap<String, String> configMapSimple = new ConfigMap<>(String.class, String.class);
public ConfigList<A> configList = new ConfigList<>(A.class);
public ConfigSet<A> configSet = new ConfigSet<>(A.class);
public ConfigMap<String, A> configMap = new ConfigMap<>(String.class, A.class);
public ConfigTypeClass() {
configListSimple.add("a");
configSetSimple.add("b");
configMapSimple.put("c", "d");
configList.add(from("e"));
configSet.add(from("f"));
configMap.put("g", from("h"));
}
public static Map<String, Object> newValues() {
Map<String, Object> map = new HashMap<>();
List<String> configListSimple = new ConfigList<>(String.class);
configListSimple.add("b");
Set<String> configSetSimple = new ConfigSet<>(String.class);
configSetSimple.add("c");
Map<String, String> configMapSimple = new ConfigMap<>(String.class, String.class);
configMapSimple.put("d", "e");
List<A> configList = new ConfigList<>(A.class);
configList.add(from("f"));
Set<A> configSet = new ConfigSet<>(A.class);
configSet.add(from("g"));
Map<String, A> configMap = new ConfigMap<>(String.class, A.class);
configMap.put("h", from("i"));
map.put("configListSimple", configListSimple);
map.put("configSetSimple", configSetSimple);
map.put("configMapSimple", configMapSimple);
map.put("configList", configList);
map.put("configSet", configSet);
map.put("configMap", configMap);
return map;
}
public static A from(String string) {
A a = new A();
a.string = string;
return a;
}
public static final class A {
private String string = "string";
@Override
public String toString() {
return "A{" +
"string='" + string + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
A a = (A) o;
return string.equals(a.string);
}
@Override
public int hashCode() {
return string.hashCode();
}
}
}

@ -1,67 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
import java.util.*;
public class DefaultTypeClass extends Configuration {
private boolean bool = true;
private char c = 'c';
private byte b = 1;
private short s = 2;
private int i = 3;
private long l = 4;
private float f = 5.0f;
private double d = 6.0;
private Boolean boolObject = true;
private Character cObject = 'c';
private Byte bObject = 1;
private Short sObject = 2;
private Integer iObject = 3;
private Long lObject = 4L;
private Float fObject = 5.0f;
private Double dObject = 6.0;
private String string = "string";
private List<String> list = new ArrayList<>();
private Set<String> set = new HashSet<>();
private Map<String, String> map = new HashMap<>();
public DefaultTypeClass(Path path) {
super(path);
list.add("a");
set.add("b");
map.put("c", "d");
}
public static Map<String, Object> newValues() {
Map<String, Object> map = new HashMap<>();
map.put("bool", false);
map.put("c", 'd');
map.put("b", (byte) 2);
map.put("s", (short) 3);
map.put("i", 4);
map.put("l", 5L);
map.put("f", 6.0f);
map.put("d", 7.0);
map.put("boolObject", false);
map.put("cObject", 'd');
map.put("bObject", (byte) 2);
map.put("sObject", (short) 3);
map.put("iObject", 4);
map.put("lObject", 5L);
map.put("fObject", 6.0f);
map.put("dObject", 7.0);
map.put("string", "string2");
List<String> list = new ArrayList<>();
list.add("b");
map.put("list", list);
Set<String> set = new HashSet<>();
set.add("c");
map.put("set", set);
Map<String, String> map2 = new HashMap<>();
map2.put("d", "e");
map.put("map", map2);
return map;
}
}

@ -1,14 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
public class NonDefaultTypeClass extends Configuration {
public DefaultTypeClass defaultTypeClass;
public NonDefaultTypeClass(Path configPath) {
super(configPath);
this.defaultTypeClass = new DefaultTypeClass(configPath);
}
}

@ -1,29 +0,0 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
public class SimpleTypesClass extends Configuration {
private boolean bool = true;
private char c = 'c';
private byte b = 1;
private short s = 2;
private int i = 3;
private long l = 4;
private float f = 5.0f;
private double d = 6.0;
private Boolean boolObject = true;
private Character cObject = 'c';
private Byte bObject = 1;
private Short sObject = 2;
private Integer iObject = 3;
private Long lObject = 4L;
private Float fObject = 5.0f;
private Double dObject = 6.0;
private String string = "string";
public SimpleTypesClass(Path configPath) {
super(configPath);
}
}

@ -0,0 +1,492 @@
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 java.util.stream.Collectors.toCollection;
@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 = linkedHashSetOf(1, 2, 3);
TEST_VALUES.strings = List.of("a", "b", "c");
TEST_VALUES.doubleByBool = linkedHashMap(true, 1.0, false, 2.0);
/* containers of other types */
TEST_VALUES.subClassSet = linkedHashSetOf(
TestSubClass.of(1, "1"), TestSubClass.of(2, "2")
);
TEST_VALUES.subClassList = List.of(
TestSubClass.of(1, "1"), TestSubClass.of(2, "2")
);
TEST_VALUES.subClassMap = linkedHashMap(
"1", TestSubClass.of(1, "1"),
"2", TestSubClass.of(2, "2")
);
/* nested containers of simple types */
TEST_VALUES.listsList = List.of(
List.of(1, 2), List.of(3, 4)
);
TEST_VALUES.setsSet = linkedHashSetOf(
linkedHashSetOf("a", "b"), linkedHashSetOf("c", "d")
);
TEST_VALUES.mapsMap = linkedHashMap(
1, Map.of("1", 1), 2, Map.of("2", 2)
);
/* nested containers of custom types */
TEST_VALUES.subClassListsList = List.of(
List.of(TestSubClass.of(1, "1"), TestSubClass.of(2, "2"))
);
TEST_VALUES.subClassSetsSet = linkedHashSetOf(linkedHashSetOf(
TestSubClass.of(1, "1"), TestSubClass.of(2, "2")
));
TEST_VALUES.subClassMapsMap = linkedHashMap(
1, Map.of("1", TestSubClass.of(1, "2")),
2, Map.of("2", TestSubClass.of(2, "2"))
);
TEST_VALUES.e1 = TestEnum.NON_DEFAULT;
TEST_VALUES.enums = List.of(TestEnum.DEFAULT, TestEnum.NON_DEFAULT);
TEST_VALUES.converterSubClass = TestSubClass.of(2, "2");
TEST_VALUES.excludedClass = TestExcludedClass.TEST_VALUES;
}
@SafeVarargs
private static <T> Set<T> linkedHashSetOf(T... values) {
return Arrays.stream(values).collect(toCollection(LinkedHashSet::new));
}
private static <K, V> Map<K, V> linkedHashMap(K k1, V v1, K k2, V v2) {
Map<K, V> map = new LinkedHashMap<>();
map.put(k1, v1);
map.put(k2, v2);
return map;
}
/* 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(TestSubClass.class)
private List<List<TestSubClass>> subClassListsList = new ArrayList<>();
@ElementType(TestSubClass.class)
private Set<Set<TestSubClass>> subClassSetsSet = new HashSet<>();
@ElementType(TestSubClass.class)
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 +
'}';
}
}

@ -0,0 +1,55 @@
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 + '\'' +
'}';
}
}

@ -0,0 +1,70 @@
package de.exlll.configlib.classes;
import de.exlll.configlib.annotation.ConfigurationElement;
import java.util.Map;
@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";
}
private final int finalInt = 1;
private int primInt;
private String string = "";
public static TestSubClass of(int primInt, String string) {
TestSubClass subClass = new TestSubClass();
subClass.primInt = primInt;
subClass.string = string;
return subClass;
}
public Map<String, Object> asMap() {
return Map.of("primInt", primInt, "string", string);
}
public int getFinalInt() {
return finalInt;
}
public int getPrimInt() {
return primInt;
}
public String getString() {
return string;
}
@Override
public String toString() {
return "TestSubClass{" +
"\nprimInt=" + primInt +
",\nstring='" + string + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TestSubClass)) return false;
TestSubClass subClass = (TestSubClass) o;
if (primInt != subClass.primInt) return false;
return string.equals(subClass.string);
}
@Override
public int hashCode() {
int result = primInt;
result = 31 * result + string.hashCode();
return result;
}
}

@ -0,0 +1,23 @@
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";
}
}

@ -0,0 +1,45 @@
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;
}
}
}

@ -0,0 +1,324 @@
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 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 java.util.List;
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() throws IOException {
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() throws IOException {
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() throws IOException {
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() throws IOException {
setupConfigPath();
Configuration configuration = new TestClass(configPath);
assertThat(configuration, is(not(TestClass.TEST_VALUES)));
configuration.load();
assertThat(configuration, is((TestClass.TEST_VALUES)));
}
private void setupConfigPath() throws IOException {
Configuration configuration = new TestClass(
configPath, TestClass.TEST_VALUES
);
configuration.save();
}
@Test
void loadThrowsExceptionIfTypesDontMatch() throws IOException {
Configuration configuration = new TestClass(configPath);
configuration.save();
assertThrows(IllegalArgumentException.class, configuration::load);
}
@Test
void saveCreatesConfig() throws IOException {
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(List.of("AB", "", "CD"))
.setAppendedComments(List.of("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));
}
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 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" +
"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" +
" : null\n" +
" ? primInt: 2\n" +
" string: '2'\n" +
" : null\n" +
"subClassList:\n" +
"- primInt: 1\n" +
" string: '1'\n" +
"- primInt: 2\n" +
" string: '2'\n" +
"subClassMap:\n" +
" '1':\n" +
" primInt: 1\n" +
" string: '1'\n" +
" '2':\n" +
" primInt: 2\n" +
" string: '2'\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: 1\n" +
" string: '1'\n" +
" - primInt: 2\n" +
" string: '2'\n" +
"subClassSetsSet: !!set\n" +
" ? !!set\n" +
" ? primInt: 1\n" +
" string: '1'\n" +
" : null\n" +
" ? primInt: 2\n" +
" string: '2'\n" +
" : null\n" +
" : null\n" +
"subClassMapsMap:\n" +
" 1:\n" +
" '1':\n" +
" primInt: 1\n" +
" string: '2'\n" +
" 2:\n" +
" '2':\n" +
" primInt: 2\n" +
" string: '2'\n" +
"e1: NON_DEFAULT\n" +
"enums:\n" +
"- DEFAULT\n" +
"- NON_DEFAULT\n" +
"converterSubClass: '2:2'\n" +
"excludedClass: !!de.exlll.configlib.classes.TestExcludedClass\n" +
" primInt: 1\n" +
" string: string";
}

@ -0,0 +1,42 @@
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 java.util.Map;
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), Map.of());
assertThat(Files.exists(parentDir), is(true));
}
}

@ -1,173 +1,457 @@
# ConfigLib
This library facilitates creating, saving and loading YAML configuration files. It does so
by using Reflection on configuration classes and automatically saving and loading their field
names and values, creating the configuration file and its parent directories if necessary.
# ConfigLib v2
**A Bukkit and BungeeCord library for storing and loading configurations**
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.
Currently this library only supports storing configurations as YAML. However, users may provide their own
storage systems.
## Features
- automatic creation, saving and loading of YAML configurations
- automatic creation of parent directories
- option to add explanatory comments by adding annotations to the class or its fields
- option to exclude fields by making them final, static or transient
- option to change the style of the configuration file
- option to version configuration files and change the way updates are applied
* 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 `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)
## General information
#### What can be serialized?
You can add fields to your configuration class whose type is one of the following:
- a simple type, which are all primitive types (e.g. `boolean`, `int`), their wrapper types (e.g.
`Boolean`, `Integer`) and strings
- `List`s, `Set`s and `Map`s of simple types (e.g `List<Double>`) or other lists, sets and maps
(e.g. `List<List<Map<String, Integer>>>`)
- custom types which have a no-argument constructor
- `ConfigList`s, `ConfigSet`s and `ConfigMap`s of custom types
If you want to use lists, sets or maps containing objects of custom types,
you have to use `ConfigList`, `ConfigSet` or `ConfigMap`, respectively. If you don't use these
special classes for storing custom objects, the stored objects won't be properly (de-)serialized.
#### Default and null values
All reference type fields of a configuration class must be assigned non-null default values.
If any value is `null`, (de-)serialization will fail with a `NullPointerException`.
#### Serialization of custom classes
You can add fields to your configuration class whose type is some custom class.
`@Comment`s added to custom classes or their fields are ignored and won't be
displayed in the configuration file.
## How-to
You can find a step-by-step tutorial here:
[Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
#### Creating a configuration
To create a new configuration, create a class which extends `Configuration`. Fields which are
not `final`, `static` or `transient` and whose type is one of the above can automatically be saved
to the corresponding configuration file.
#### Saving and loading a configuration
#### 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_)
#### 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` updates all fields of an instance with the values read from the configuration file.
- `save` dumps all field names and values to a configuration file. If the file exists, it is
overridden; otherwise, it is created.
- `loadAndSave` first calls `load` and then `save`, which is useful when you have added or
removed fields from the class or you simply don't know if the configuration file exists.
- `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
In order to add or to remove fields, you just need to add them to or remove them from your
configuration class. The changes are saved to the configuration file the next time `save` or
`loadAndSave` is called.
#### Post load action
You can override `postLoadHook` to execute some action after the configuration has successfully
been loaded.
#### Comments
By using the `@Comment` annotation, you can add comments to your configuration file. The
annotation can be applied to classes or fields. Each `String` of the passed array is
written into a new line.
#### Versioning
Use the `@Version` annotation to enable versioning. Versioning lets you change the way
how configuration files are updated when a version change is detected.
#### Custom configuration style
You can change the style of the configuration file by overriding the protected `create...` methods
of your configuration class. Overriding these methods effectively changes the behavior of the
underlying `Yaml` parser. Note that if one these methods returns `null`, a `NullPointerException`
will be thrown.
For more information, consult the official
[documentation](https://bitbucket.org/asomov/snakeyaml/wiki/Documentation).
## Examples
Step-by-step tutorial: [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
#### Example of a custom class
```java
public class Credentials {
private String username = "minecraft";
private String password = "secret";
}
```
#### Example database configuration
```java
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
/* other imports */
@Comment({
"This is a multiline comment.",
"It describes what the configuration is about."
})
@Version(version = "1.2.3")
public final class DatabaseConfig extends Configuration {
/* ignored fields */
private final String ignored1 = ""; // ignored because final
private static String ignored2 = ""; // ignored because static
private transient String ignored3 = ""; // ignored because transient
/* included fields */
private String host = "localhost";
private int port = 3306;
@Comment("This is a single-line comment.")
private List<String> strings = Arrays.asList("root", "local");
@Comment({
"This is a multiline comment.",
"It describes what this field does."
})
private Map<String, List<String>> listByStrings = new HashMap<>();
private Credentials credentials = new Credentials();
public DatabaseConfig(Path configPath) {
super(configPath);
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).
#### 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 = "";
// ...
}
```
Enums are supported:
```java
class MyConfiguration extends YamlConfiguration {
private Material material = Material.AIR;
//...
}
```
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.
```java
@ConfigurationElement
class MyCustomClass1 {/* fields etc.*/}
@ConfigurationElement
class MyCustomClass2 {
private MyCustomClass1 cls1 = new MyCustomClass1();
// ...
}
class MyConfiguration extends YamlConfiguration {
private MyCustomClass2 cls2 = new MyCustomClass2();
// ...
}
```
#### `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<>();
// ...
}
```
Note: Even though sets are supported, their YAML-representation is 'pretty ugly', so it's better to use lists instead.
If you need the set behavior, you can internally use lists and convert them to sets using the `preSave/postLoad`-hooks.
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.
```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<>();
// ...
}
```
Lists, sets and maps can be nested.
```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(MyCustomClass.class)
private List<List<MyCustomClass>> customClassListsList = new ArrayList<>();
@ElementType(MyCustomClass.class)
private Set<Set<MyCustomClass>> customClassSetsSet = new HashSet<>();
@ElementType(MyCustomClass.class)
private Map<Integer, Map<String, MyCustomClass>> customClassMapsMap = new HashMap<>();
// ...
}
```
#### 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;
// ...
}
```
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 ... */}
// ...
}
```
#### 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.
```java
class MyConfiguration extends BukkitYamlConfiguration {
@NoConvert
private ItemStack itemStack = new ItemStack(Material.STONE, 1);
// ...
}
```
#### Changing configuration properties
To change the properties of a configuration, use the properties builder object.
##### 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.
```java
YamlProperties properties = YamlProperties.builder()
.setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
// ...
.build();
```
Note: You should neither remove nor replace a formatter with one that has a different formatting style because this
could break existing configurations.
##### (_YAML_) Prepending/appending text
To prepend or append comments to a configuration file, use the `setPrependedComments` and `setAppendedComments` methods,
respectively.
```java
YamlProperties properties = YamlProperties.builder()
.setPrependedComments(Arrays.asList("A", "B"))
.setAppendedComments(Arrays.asList("C", "D"))
// ...
.build();
```
##### (_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).
```java
YamlProperties properties = YamlProperties.builder()
.setConstructor(...)
.setRepresenter(/* */)
.setOptions(/* */)
.setResolver(/* */)
// ...
.build();
```
Note: Changing the configuration style may break adding comments using the `@Comment` annotation.
#### 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).
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.
```java
import java.awt.Point;
class PointStringConverter implements Converter<Point, String> {
@Override
public String convertTo(Point element, ConversionInfo info) {
return element.x + ":" + element.y;
}
@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);
}
/* other methods */
}
```
#### Example Bukkit plugin
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);
//...
}
```
Note: Only a single converter instance is created which is cached.
## Example
```java
public class ExamplePlugin extends JavaPlugin {
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
}
Credentials(String username, String password) {
this.username = username;
this.password = password;
}
String getUsername() { return username; }
}
@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);
}
Credentials getAdminAccount() { return adminAccount; }
}
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();
Path configPath = new File(getDataFolder(), "config.yml").toPath();
DatabaseConfig config = new DatabaseConfig(configPath);
try {
config.loadAndSave();
System.out.println(config.getPort());
} catch (IOException e) {
/* do something with exception */
}
DatabaseConfig config = new DatabaseConfig(configPath, props);
config.loadAndSave();
System.out.println(config.getAdminAccount().getUsername());
}
}
```
## Import
#### Maven
```xml
<repository>
<id>de.exlll</id>
<url>https://repo.exlll.de/artifactory/releases/</url>
<url>http://exlll.de:8081/artifactory/releases/</url>
</repository>
<!-- for Bukkit plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bukkit</artifactId>
<version>1.4.1</version>
<version>2.0.0</version>
</dependency>
<!-- for Bungee plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bungee</artifactId>
<version>1.4.1</version>
<version>2.0.0</version>
</dependency>
```
#### Gradle
```groovy
repositories {
maven {
url 'https://repo.exlll.de/artifactory/releases/'
url 'http://exlll.de:8081/artifactory/releases/'
}
}
dependencies {
// for Bukkit plugins
compile group: 'de.exlll', name: 'configlib-bukkit', version: '1.4.1'
compile group: 'de.exlll', name: 'configlib-bukkit', version: '2.0.0'
// for Bungee plugins
compile group: 'de.exlll', name: 'configlib-bungee', version: '1.4.1'
compile group: 'de.exlll', name: 'configlib-bungee', version: '2.0.0'
}
```
Additionally, you either have to import the Bukkit or BungeeCord API
or disable transitive lookups. This project uses both of these APIs, so if you
need an example of how to import them with Gradle, take a look at the `build.gradle`.
If, for some reason, you have SSL errors that you're unable to resolve, you can
use `http://exlll.de:8081/artifactory/releases/` as the repository instead.

@ -1,6 +1,6 @@
allprojects {
group 'de.exlll'
version '1.4.1'
version '2.0.0'
}
subprojects {
apply plugin: 'java'
@ -8,7 +8,9 @@ subprojects {
repositories { mavenCentral() }
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.0.3'
testCompile group: 'org.junit.platform', name: 'junit-platform-runner', version: '1.0.3'
testCompile group: 'org.junit.platform', name: 'junit-platform-suite-api', version: '1.0.3'
testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
testCompile group: 'com.google.jimfs', name: 'jimfs', version: '1.1'
}
@ -23,13 +25,13 @@ if (project.hasProperty("local_script")) {
}
project(':configlib-core') {
dependencies { compile group: 'org.yaml', name: 'snakeyaml', version: '1.17' }
dependencies { compile group: 'org.yaml', name: 'snakeyaml', version: '1.20' }
}
project(':configlib-bukkit') {
repositories { maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } }
dependencies {
compile project(':configlib-core')
compile group: 'org.bukkit', name: 'bukkit', version: '1.11.2-R0.1-SNAPSHOT'
compile group: 'org.bukkit', name: 'bukkit', version: '1.12.2-R0.1-SNAPSHOT'
}
jar { from { project(':configlib-core').sourceSets.main.output } }
}
@ -37,7 +39,7 @@ project(':configlib-bungee') {
repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } }
dependencies {
compile project(':configlib-core')
compile group: 'net.md-5', name: 'bungeecord-api', version: '1.10-SNAPSHOT'
compile group: 'net.md-5', name: 'bungeecord-api', version: '1.12-SNAPSHOT'
}
jar { from { project(':configlib-core').sourceSets.main.output } }
}

@ -1,248 +0,0 @@
## Creating a configuration
Let's say you want to create the following web chat configuration:
```yaml
# This is the default WebChat configuration
# Author: John Doe
ipAddress: 127.0.0.1
port: 12345
# Usernames mapped to users
users:
User1:
email: user1@example.com
credentials:
username: User1
password: '12345'
User2:
email: user2@example.com
credentials:
username: User2
password: '54321'
User3:
email: user3@example.com
credentials:
username: User3
password: '51423'
channels:
- id: 1
name: Channel1
owner: User1
members:
- User2
- User3
- id: 2
name: Channel2
owner: User2
members:
- User1
# Current version - DON'T TOUCH!
my_version: '1.2.3-alpha'
```
### 1. Create a configuration
Create a class which extends `de.exlll.configlib.Configuration`.
```java
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
public final class WebchatConfig extends Configuration {
public WebchatConfig(Path configPath) {
super(configPath);
}
}
```
### 2. Create custom classes
Create some classes to hold the necessary information and assign default values to their
fields. Be aware that custom classes must have a no-arguments constructor.
```java
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
import java.util.List;
public final class WebchatConfig extends Configuration {
public WebchatConfig(Path configPath) {
super(configPath);
}
public static final class User {
private String email = "";
private Credentials credentials = new Credentials();
}
public static final class Credentials {
private String username = "";
private String password = "";
}
public static final class Channel {
private int id; // channel id
private String name = ""; // channel name
private String owner = ""; // username of the owner
private List<String> members = new ArrayList<>(); // other usernames
}
}
```
### 3. Add fields
You can add fields to a configuration class whose type is one of the following:
- a simple type, which are all primitive types (e.g. `boolean`, `int`), their wrapper types (e.g.
`Boolean`, `Integer`) and strings
- `List`s, `Set`s and `Map`s of simple types (e.g `List<Double>`) or other lists, sets and maps
(e.g. `List<List<Map<String, Integer>>>`)
- custom types which have a no-argument constructor,
- `ConfigList`s, `ConfigSet`s and `ConfigMap`s of custom types
If you want to use lists, sets or maps containing objects of custom types,
you have to use `ConfigList`, `ConfigSet` or `ConfigMap`, respectively. If you don't use these
special classes for storing custom objects, the stored objects won't be properly (de-)serialized.
If you don't want a field to be serialized, make it `final`, `static` or `transient`.
**NOTE:** all field values _must_ be non-null. If any value is `null`, serialization
will fail with a `NullPointerException`.
```java
import de.exlll.configlib.ConfigList;
import de.exlll.configlib.ConfigMap;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public final class WebchatConfig extends Configuration {
// private String s; // fails with a NullPointerException if not assigned a value
private String ipAddress = "127.0.0.1";
private int port = 12345;
private Map<String, User> users = new ConfigMap<>(String.class, User.class);
private List<Channel> channels = new ConfigList<>(Channel.class);
public WebchatConfig(Path configPath) {
super(configPath);
}
// ... remainder unchanged
}
```
### 4. Add comments
Comments can only be added to the configuration class or its fields.
Comments you add to other custom classes or their fields will be ignored.
```java
import de.exlll.configlib.Comment;
import de.exlll.configlib.ConfigList;
import de.exlll.configlib.ConfigMap;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
@Comment({
"This is the default WebChat configuration",
"Author: John Doe"
})
public final class WebchatConfig extends Configuration {
private String ipAddress = "127.0.0.1";
private int port = 12345;
@Comment("Usernames mapped to users")
private Map<String, User> users = new ConfigMap<>(String.class, User.class);
private List<Channel> channels = new ConfigList<>(Channel.class);
public WebchatConfig(Path configPath) {
super(configPath);
}
/* other classes and methods */
}
```
### 5. Add default values
Add some default values for lists, sets and maps.
```java
/* imports */
public final class WebchatConfig extends Configuration {
/* fields */
public WebchatConfig(Path configPath) {
super(configPath);
Channel channel1 = createNewChannel(1, "Channel1", "User1",
Arrays.asList("User2", "User3"));
Channel channel2 = createNewChannel(2, "Channel2", "User2",
Arrays.asList("User1"));
channels.add(channel1);
channels.add(channel2);
User user1 = createNewUser("user1@example.com", "User1", "12345");
User user2 = createNewUser("user2@example.com", "User2", "54321");
User user3 = createNewUser("user3@example.com", "User3", "51423");
users.put(user1.credentials.username, user1);
users.put(user2.credentials.username, user2);
users.put(user3.credentials.username, user3);
}
private Channel createNewChannel(int id, String name, String owner,
List<String> members) {
Channel channel = new Channel();
channel.id = id;
channel.name = name;
channel.owner = owner;
channel.members = members;
return channel;
}
private User createNewUser(String email, String username, String password) {
User user = new User();
user.email = email;
user.credentials.username = username;
user.credentials.password = password;
return user;
}
/* other classes and methods */
}
```
### 6. Add the version
```java
import de.exlll.configlib.Configuration;
import de.exlll.configlib.Version;
import java.nio.file.Path;
@Version(
version = "1.2.3-alpha",
fieldName = "my_version",
fieldComments = {
"" /* empty line */,
"Current version - DON'T TOUCH!"
}
)
public final class WebchatConfig extends Configuration {
// ... remainder unchanged
}
```
### 7. Create an instance of your configuration
Create a `java.nio.file.Path` object and pass it to the configuration constructor.
```java
/* imports */
public final class WebchatConfig extends Configuration {
/*...*/
public static void main(String[] args) {
File configFolder = new File("folder");
File configFile = new File(configFolder, "config.yml");
Path path = configFile.toPath();
/* of course, you can skip the above steps and directly
* create a Path object using Paths.get(...) */
WebchatConfig config = new WebchatConfig(path);
try {
config.loadAndSave();
System.out.println(config.getIpAddress());
System.out.println(config.getPort());
} catch (IOException e) {
e.printStackTrace();
}
}
}
```

@ -0,0 +1,7 @@
This section covers some the advanced features of this library.
## Converters
## Configuration sources
Loading…
Cancel
Save