better support for custom classes

dev
Exlll 8 years ago
parent 0dca3f614f
commit 0e9b474db1

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

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

@ -7,10 +7,8 @@ import java.lang.annotation.Target;
/**
* The {@code Comment} annotation can be used to add comments to a configuration file.
* <p>
* Each {@code String} is written into a new line.
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Comment {
String[] value();

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

@ -8,41 +8,34 @@ import static java.util.stream.Collectors.toMap;
final class Comments {
private final List<String> classComments;
private final Map<String, List<String>> commentsByFieldNames;
private final Map<String, List<String>> fieldComments;
Comments(List<String> classComments,
Map<String, List<String>> commentsByFieldName) {
this.classComments = classComments;
this.commentsByFieldNames = commentsByFieldName;
Comments(Class<?> cls) {
this.classComments = getComments(cls);
this.fieldComments = getFieldComments(cls);
}
static Comments from(FilteredFieldStreamSupplier supplier) {
Objects.requireNonNull(supplier);
List<String> classComments = getComments(supplier.getSupplyingClass());
Map<String, List<String>> commentsByFieldNames = supplier
.get()
.filter(Comments::hasCommentAnnotation)
.collect(toMap(Field::getName, Comments::getComments));
return new Comments(classComments, commentsByFieldNames);
}
static List<String> getComments(AnnotatedElement element) {
private List<String> getComments(AnnotatedElement element) {
Comment comment = element.getAnnotation(Comment.class);
return (comment != null) ?
Arrays.asList(comment.value()) :
return (comment != null) ? Arrays.asList(comment.value()) :
Collections.emptyList();
}
static boolean hasCommentAnnotation(AnnotatedElement element) {
private Map<String, List<String>> getFieldComments(Class<?> cls) {
return Arrays.stream(cls.getDeclaredFields())
.filter(this::hasCommentAnnotation)
.collect(toMap(Field::getName, this::getComments));
}
private boolean hasCommentAnnotation(AnnotatedElement element) {
return element.isAnnotationPresent(Comment.class);
}
List<String> getClassComments() {
public List<String> getClassComments() {
return classComments;
}
Map<String, List<String>> getCommentsByFieldNames() {
return commentsByFieldNames;
public Map<String, List<String>> getFieldComments() {
return fieldComments;
}
}

@ -0,0 +1,16 @@
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");
return scanner.next();
}
}
}

@ -0,0 +1,16 @@
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,19 +1,24 @@
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 java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Map;
import java.util.Objects;
public abstract class Configuration {
private final Path configPath;
private final FieldMapper fieldMapper;
private final ConfigurationWriter writer;
private final YamlSerializer serializer;
private final CommentAdder adder;
/**
* Creates a new {@code Configuration} instance.
@ -24,32 +29,26 @@ public abstract class Configuration {
* @param configPath location of the configuration file
* @throws NullPointerException if {@code configPath} is null
*/
public Configuration(Path configPath) {
Objects.requireNonNull(configPath);
FilteredFieldStreamSupplier ffss = new FilteredFieldStreamSupplier(
getClass(), ConfigurationFieldFilter.INSTANCE);
Comments comments = Comments.from(ffss);
protected Configuration(Path configPath) {
this.configPath = configPath;
this.fieldMapper = new FieldMapper(ffss);
this.writer = new ConfigurationWriter(configPath, comments);
this.serializer = new YamlSerializer();
ffss.fieldTypes().forEach(serializer::addTagIfClassUnknown);
this.serializer = new YamlSerializer(
createConstructor(), createRepresenter(),
createDumperOptions(), createResolver()
);
this.adder = new CommentAdder(new Comments(getClass()));
}
/**
* Loads the configuration file from the specified {@code Path} and sets {@code this} fields.
* Loads the configuration file from the specified {@code Path} and updates this attribute values.
*
* @throws ClassCastException if parsed Object is not a {@code Map}
* @throws IOException if an I/O error occurs when loading the configuration file.
* @throws ParserException if invalid YAML
*/
public final void load() throws IOException {
String dump = new ConfigurationReader(configPath).read();
Map<String, Object> valuesByFieldNames = serializer.deserialize(dump);
fieldMapper.mapValuesToFields(valuesByFieldNames, this);
postLoadHook();
String yaml = ConfigReader.read(configPath);
Map<String, Object> deserializedMap = serializer.deserialize(yaml);
FieldMapper.instanceFromMap(this, deserializedMap);
}
/**
@ -65,13 +64,19 @@ public abstract class Configuration {
public final void save() throws IOException {
createParentDirectories();
Map<String, Object> valuesByFieldNames = fieldMapper.mapFieldNamesToValues(this);
String dump = serializer.serialize(valuesByFieldNames);
writer.write(dump);
Map<String, Object> map = FieldMapper.instanceToMap(this);
String serializedMap = serializer.serialize(map);
ConfigWriter.write(configPath, adder.addComments(serializedMap));
}
private void createParentDirectories() throws IOException {
Files.createDirectories(configPath.getParent());
}
/**
* Loads and saves the configuration file.
* <p>
* This method first calls {@link #load()} and then {@link #save()}.
*
* @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.
@ -83,9 +88,9 @@ public abstract class Configuration {
try {
load();
save();
} catch (NoSuchFileException e) {
save();
} catch (NoSuchFileException | FileNotFoundException e) {
postLoadHook();
save();
}
}
@ -100,7 +105,62 @@ public abstract class Configuration {
protected void postLoadHook() {
}
private void createParentDirectories() throws IOException {
Files.createDirectories(configPath.getParent());
/**
* Creates a {@code 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.
*
* @return new {@code BaseConstructor}
* @see org.yaml.snakeyaml.constructor.BaseConstructor
*/
protected BaseConstructor createConstructor() {
return new Constructor();
}
/**
* Creates a {@code Representer} 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 new {@code Representer}
* @see org.yaml.snakeyaml.representer.Representer
*/
protected Representer createRepresenter() {
return new Representer();
}
/**
* Creates a {@code 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.
* <p>
* This method may not return null.
*
* @return new {@code DumperOptions}
* @see org.yaml.snakeyaml.DumperOptions
*/
protected DumperOptions createDumperOptions() {
DumperOptions options = new DumperOptions();
options.setIndent(2);
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
return options;
}
/**
* Creates a {@code 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 new {@code Resolver}
* @see org.yaml.snakeyaml.resolver.Resolver
*/
protected Resolver createResolver() {
return new Resolver();
}
}

@ -1,18 +0,0 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.function.Predicate;
enum ConfigurationFieldFilter implements Predicate<Field> {
INSTANCE;
@Override
public boolean test(Field field) {
int modifiers = field.getModifiers();
boolean fst = Modifier.isFinal(modifiers) ||
Modifier.isStatic(modifiers) ||
Modifier.isTransient(modifiers);
return !(field.isSynthetic() || fst);
}
}

@ -1,19 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;
final class ConfigurationReader {
private final Path path;
ConfigurationReader(Path path) {
this.path = path;
}
String read() throws IOException {
try (Scanner scanner = new Scanner(path)) {
return scanner.useDelimiter("\\z").next();
}
}
}

@ -1,77 +0,0 @@
package de.exlll.configlib;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
final class ConfigurationWriter {
private final Path configPath;
private final Comments comments;
private Writer writer;
ConfigurationWriter(Path configPath, Comments comments) {
this.configPath = configPath;
this.comments = comments;
}
void write(String dump) throws IOException {
writer = Files.newBufferedWriter(configPath);
writeClassComments();
writeDump(dump);
writer.close();
}
private void writeClassComments() throws IOException {
List<String> classComments = comments.getClassComments();
if (!classComments.isEmpty()) {
writeComments(classComments);
writer.write('\n');
}
}
private void writeComments(List<String> comments) throws IOException {
for (String comment : comments) {
writer.write("# ");
writer.write(comment);
writer.write('\n');
}
}
private void writeDump(String dump) throws IOException {
String[] dumpLines = dump.split("\n");
for (String dumpLine : dumpLines) {
writeLine(dumpLine);
}
}
private void writeLine(String line) throws IOException {
writeFieldComment(line);
writer.write(line);
writer.write('\n');
}
private void writeFieldComment(String line) throws IOException {
if (!line.contains(":")) {
return;
}
Optional<List<String>> cmts = getFieldComments(line);
if (cmts.isPresent()) {
writeComments(cmts.get());
}
}
private Optional<List<String>> getFieldComments(String line) {
return comments.getCommentsByFieldNames()
.entrySet()
.stream()
.filter(entry -> line.startsWith(entry.getKey() + ":"))
.map(Map.Entry::getValue)
.findAny();
}
}

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

@ -2,65 +2,100 @@ package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
enum FieldMapper {
;
private static final Set<Class<?>> defaultClasses = Stream.of(
Boolean.class, String.class, Character.class,
Byte.class, Short.class, Integer.class, Long.class,
Float.class, Double.class
).collect(Collectors.toSet());
final class FieldMapper {
private final FilteredFieldStreamSupplier streamSupplier;
static Map<String, Object> instanceToMap(Object instance) {
Map<String, Object> map = new LinkedHashMap<>();
FieldMapper(FilteredFieldStreamSupplier streamSupplier) {
Objects.requireNonNull(streamSupplier);
this.streamSupplier = streamSupplier;
}
FilteredFields ff = FilteredFields.of(instance.getClass());
for (Field field : ff) {
Object value = getValue(field, instance);
checkNull(field, value);
value = isDefault(field.getType()) ? value : instanceToMap(value);
map.put(field.getName(), value);
}
Map<String, Object> mapFieldNamesToValues(Object instance) {
return streamSupplier.get().collect(
toMap(Field::getName,
field -> getValue(field, instance),
(f1, f2) -> f1,
LinkedHashMap::new));
return map;
}
private Object getValue(Field field, Object instance) {
try {
field.setAccessible(true);
Object value = field.get(instance);
checkNull(field, value);
return field.get(instance);
} catch (IllegalAccessException e) {
/* cannot happen */
throw new AssertionError(e);
static void instanceFromMap(Object instance, Map<String, ?> map) {
FilteredFields ff = FilteredFields.of(instance.getClass());
for (Field field : ff) {
Object val = map.get(field.getName());
if (val == null) {
continue; // keep default value
}
if (isDefault(field.getType())) {
setValue(field, instance, val);
} else {
Object inst = getValue(field, instance);
instanceFromMap(inst, castToMap(val));
}
}
}
private void checkNull(Field field, Object o) {
private static void checkNull(Field field, Object o) {
if (o == null) {
String msg = String.format("The value of field %s is null.\n" +
"Please assign a non-null default value or remove this field.", field);
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);
}
}
void mapValuesToFields(Map<String, Object> valuesByFieldNames, Object instance) {
for (Field field : streamSupplier.toList()) {
String fieldName = field.getName();
if (valuesByFieldNames.containsKey(fieldName)) {
Object value = valuesByFieldNames.get(fieldName);
setField(field, instance, value);
}
private static Map<String, Object> castToMap(Object map) {
@SuppressWarnings("unchecked")
Map<String, Object> m = (Map<String, Object>) map;
return m;
}
private static boolean isDefault(Class<?> cls) {
// all default types are properly handled by the YamlSerializer
return cls.isPrimitive() || isDefaultClass(cls) || isDefaultType(cls);
}
private static boolean isDefaultClass(Class<?> cls) {
return defaultClasses.contains(cls);
}
private static boolean isDefaultType(Class<?> cls) {
return Map.class.isAssignableFrom(cls) ||
Set.class.isAssignableFrom(cls) ||
List.class.isAssignableFrom(cls);
}
static Object getValue(Field field, Object instance) {
try {
field.setAccessible(true);
return field.get(instance);
} catch (IllegalAccessException e) {
/* This exception is never thrown because we filter
* inaccessible fields out. */
throw new RuntimeException(e);
}
}
private void setField(Field field, Object instance, Object value) {
static void setValue(Field field, Object instance, Object value) {
try {
field.setAccessible(true);
field.set(instance, value);
} catch (IllegalAccessException e) {
/* This exception is only thrown when the field is "static final".
* Since this library filters "static final" fields out, the
* exception is never thrown in production. */
/* This exception is never thrown because we filter
* inaccessible fields out. */
throw new RuntimeException(e);
}
}

@ -1,41 +0,0 @@
package de.exlll.configlib;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
final class FilteredFieldStreamSupplier implements Supplier<Stream<Field>> {
private final Class<?> cls;
private final Supplier<Stream<Field>> streamSupplier;
FilteredFieldStreamSupplier(Class<?> cls, Predicate<Field> fieldFilter) {
Objects.requireNonNull(cls);
Objects.requireNonNull(fieldFilter);
this.cls = cls;
Field[] fields = cls.getDeclaredFields();
streamSupplier = () -> Arrays.stream(fields).filter(fieldFilter);
}
@Override
public Stream<Field> get() {
return streamSupplier.get();
}
public List<Class<?>> fieldTypes() {
return streamSupplier.get().map(Field::getType)
.collect(Collectors.toList());
}
public List<Field> toList() {
return streamSupplier.get().collect(Collectors.toList());
}
public Class<?> getSupplyingClass() {
return cls;
}
}

@ -0,0 +1,32 @@
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,68 +1,62 @@
package de.exlll.configlib;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.Tag;
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.*;
import java.util.Map;
import java.util.Objects;
final class YamlSerializer {
static final Set<Class<?>> DEFAULT_CLASSES = new HashSet<>();
static final DumperOptions DUMPER_OPTIONS = new DumperOptions();
private final Constructor constructor = new Constructor();
private final Representer representer = new Representer();
private final Yaml yaml = new Yaml(constructor, representer, DUMPER_OPTIONS);
private final Set<Class<?>> knownClasses = new HashSet<>();
private final Yaml yaml;
static {
Class<?>[] classes = {
Boolean.class, Long.class, Integer.class, Short.class, Byte.class,
Double.class, Float.class, String.class, Character.class
};
DEFAULT_CLASSES.addAll(Arrays.asList(classes));
DUMPER_OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
DUMPER_OPTIONS.setIndent(2);
/**
* @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);
}
String serialize(Map<String, Object> mapToDump) {
mapToDump.forEach((fieldName, fieldValue) -> addTagIfClassUnknown(fieldValue.getClass()));
return yaml.dump(mapToDump);
/**
* 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);
}
/**
* @throws ParserException if invalid YAML
* @throws ClassCastException if parsed Object is not a {@code 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 stringToLoad) {
Map<String, Object> deserialize(String serializedMap) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) yaml.load(stringToLoad);
Map<String, Object> map = (Map<String, Object>) yaml.load(serializedMap);
return map;
}
void addTagIfClassUnknown(Class<?> valueClass) {
if (!isKnown(valueClass)) {
knownClasses.add(valueClass);
Tag tag = new Tag("!" + valueClass.getSimpleName());
constructor.addTypeDescription(new TypeDescription(valueClass, tag));
representer.addClassTag(valueClass, tag);
}
}
boolean isDefaultInstance(Class<?> c) {
return Set.class.isAssignableFrom(c) || Map.class.isAssignableFrom(c) ||
List.class.isAssignableFrom(c);
}
boolean isDefaultClass(Class<?> c) {
return DEFAULT_CLASSES.contains(c);
}
boolean isKnown(Class<?> cls) {
return knownClasses.contains(cls) || isDefaultClass(cls) || isDefaultInstance(cls);
}
}

@ -4,13 +4,13 @@ import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
CommentsTest.class,
ConfigurationFieldFilterTest.class,
ConfigurationReaderTest.class,
ConfigurationTest.class,
ConfigurationWriterTest.class,
ConfigReadWriteTest.class,
CommentAdderTest.class,
CommentsTest.class,
FieldFilterTest.class,
FieldMapperTest.class,
FilteredFieldStreamSupplierTest.class,
FilteredFieldsTest.class,
YamlSerializerTest.class
})
public class ConfigLibTestSuite {

@ -0,0 +1,26 @@
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,88 +1,31 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.lang.reflect.Field;
import java.nio.file.FileSystem;
import java.util.*;
import java.util.function.Predicate;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
public class CommentsTest {
@Rule
public ExpectedException exception = ExpectedException.none();
private static final FileSystem fs = Jimfs.newFileSystem();
private static final Predicate<Field> TRUE = x -> true;
@Test
public void factoryRequiresNonNullSupplier() throws Exception {
exception.expect(NullPointerException.class);
Comments.from(null);
@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 factoryReturnsNotNull() throws Exception {
Comments comments = Comments.from(new FilteredFieldStreamSupplier(
getClass(), TRUE));
assertThat(comments, notNullValue());
}
@Test
public void factoryReturnsCommentsWithClassComments() throws Exception {
Comments comments = Comments.from(new FilteredFieldStreamSupplier(
TestClassWithComment.class, TRUE));
List<String> classComments = Collections.singletonList("This is a class comment.");
assertThat(comments.getClassComments(), is(classComments));
}
@Test
public void factoryReturnsCommentsWithFilteredFieldComments() throws Exception {
Comments comments = Comments.from(new FilteredFieldStreamSupplier(
TestClass.class, TRUE));
/* Fields which don't have a Comment annotation are filtered out. */
Map<String, List<String>> commentsByFieldName = new HashMap<>();
commentsByFieldName.put("field1", Collections.singletonList("Field1"));
commentsByFieldName.put("field2", Arrays.asList("Field2", "field2"));
assertThat(comments.getCommentsByFieldNames(), is(commentsByFieldName));
}
public void getCommentsReturnsComments() throws Exception {
Comments comments = new Comments(TestClass.class);
@Test
public void getCommentsReturnsEmptyListIfNotCommentsPresent() throws Exception {
List<String> comments = Comments.getComments(TestClassWithoutComment.class);
assertThat(comments, is(Collections.emptyList()));
}
@Test
public void getCommentsReturnsCommentsAsList() throws Exception {
List<String> comments = Comments.getComments(TestClassWithComment.class);
assertThat(comments, is(Collections.singletonList("This is a class comment.")));
}
@Comment("This is a class comment.")
private static final class TestClassWithComment {
}
private static final class TestClassWithoutComment {
}
assertThat(comments.getClassComments(), is(Arrays.asList("a", "b")));
private static final class TestClass {
@Comment("Field1")
private int field1;
@Comment({"Field2", "field2"})
private int field2;
private int field3;
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());
}
}

@ -0,0 +1,29 @@
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,68 +0,0 @@
package de.exlll.configlib;
import org.junit.Before;
import org.junit.Test;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class ConfigurationFieldFilterTest {
private static final Supplier<Stream<Field>> streamSupplier = new FilteredFieldStreamSupplier(
TestClass.class, ConfigurationFieldFilter.INSTANCE);
private Stream<Field> fieldSupplier;
@Before
public void setUp() throws Exception {
fieldSupplier = streamSupplier.get();
}
@Test
public void filterFiltersSyntheticFields() throws Exception {
Supplier<Stream<Field>> streamSupplier = new FilteredFieldStreamSupplier(
TestClassSynthetic.class, ConfigurationFieldFilter.INSTANCE);
streamSupplier.get().forEach(field -> assertThat(field.isSynthetic(), is(false)));
}
@Test
public void filterFiltersFinalFields() throws Exception {
fieldSupplier.forEach(field -> assertThat(
Modifier.isFinal(field.getModifiers()), is(false)));
}
@Test
public void filterFiltersStaticFields() throws Exception {
fieldSupplier.forEach(field -> assertThat(
Modifier.isStatic(field.getModifiers()), is(false)));
}
@Test
public void filterFiltersTransientFields() throws Exception {
fieldSupplier.forEach(field -> assertThat(
Modifier.isTransient(field.getModifiers()), 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;
}
}

@ -1,32 +0,0 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.Before;
import org.junit.Test;
import java.io.Writer;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class ConfigurationReaderTest {
private static final FileSystem fs = Jimfs.newFileSystem();
private ConfigurationReader reader;
@Before
public void setUp() throws Exception {
Path configPath = fs.getPath("/config.yml");
Writer writer = Files.newBufferedWriter(configPath);
writer.write(TestConfiguration.CONFIG_AS_STRING);
writer.close();
reader = new ConfigurationReader(configPath);
}
@Test
public void readFileReadsFile() throws Exception {
assertThat(reader.read(), is(TestConfiguration.CONFIG_AS_STRING));
}
}

@ -1,137 +1,49 @@
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.CoreMatchers.is;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class ConfigurationTest {
@Rule
public ExpectedException exception = ExpectedException.none();
private static final FileSystem fs = Jimfs.newFileSystem();
private Path filePath;
private FileSystem fileSystem;
private Path configPath;
@Before
public void setUp() throws Exception {
filePath = fs.getPath("/dir1/dir2/file1");
Files.deleteIfExists(filePath);
Files.deleteIfExists(filePath.getParent());
fileSystem = Jimfs.newFileSystem();
configPath = fileSystem.getPath("/a/b/config.yml");
}
@Test
public void constructorRequiresNonNullPath() throws Exception {
exception.expect(NullPointerException.class);
new Configuration(null) {
};
@After
public void tearDown() throws Exception {
fileSystem.close();
}
@Test
public void saveCreatesParentDirectories() throws Exception {
Path dirPath = filePath.getParent();
assertThat(Files.notExists(dirPath), is(true));
new Configuration(filePath) {
}.save();
assertThat(Files.exists(dirPath), is(true));
}
@Test
public void saveDoesntThrowExceptionIfDirectoryAlreadyCreated() throws Exception {
Configuration configuration = new Configuration(filePath) {
};
configuration.save();
configuration.save();
}
@Test
public void save() throws Exception {
new TestConfiguration(filePath).save();
String config = new ConfigurationReader(filePath).read();
assertThat(config, is(TestConfiguration.CONFIG_AS_STRING));
TestConfiguration cfg = new TestConfiguration(configPath);
assertThat(Files.exists(configPath.getParent()), is(false));
cfg.save();
assertThat(Files.exists(configPath.getParent()), is(true));
}
@Test
public void load() throws Exception {
new OriginalTestClass(filePath).save();
ChangedTestClass cls = new ChangedTestClass(filePath);
cls.loadAndSave();
assertThat(cls.a, is(0));
assertThat(cls.b, is(1));
assertThat(cls.c, is(4));
}
@Test
public void loadAndSaveSavesFile() throws Exception {
new OriginalTestClass(filePath).loadAndSave();
assertThat(Files.exists(filePath), is(true));
}
@Test
public void postLoadHookExecutedAfterLoad() throws Exception {
HookTestClass cls = new HookTestClass(filePath);
cls.save();
assertThat(cls.hookCalled, is(false));
cls.load();
assertThat(cls.hookCalled, is(true));
}
@Test
public void postLoadHookExecutedAfterLoadAndSaveIfPathNotExists() throws Exception {
HookTestClass cls = new HookTestClass(filePath);
assertThat(cls.hookCalled, is(false));
cls.loadAndSave();
assertThat(cls.hookCalled, is(true));
}
@Test
public void postLoadHookExecutedAfterLoadAndSaveIfPathExists() throws Exception {
HookTestClass cls = new HookTestClass(filePath);
cls.save();
assertThat(cls.hookCalled, is(false));
cls.loadAndSave();
assertThat(cls.hookCalled, is(true));
}
private static final class HookTestClass extends Configuration {
private transient boolean hookCalled;
public HookTestClass(Path configPath) {
super(configPath);
}
@Override
protected void postLoadHook() {
hookCalled = true;
}
}
private static final class OriginalTestClass extends Configuration {
private int a = 0;
private int b = 1;
public OriginalTestClass(Path configPath) {super(configPath);}
}
private static final class ChangedTestClass extends Configuration {
private int a = 2;
private int b = 3;
private int c = 4;
public void saveWritesConfig() throws Exception {
TestConfiguration cfg = new TestConfiguration(configPath);
assertThat(Files.exists(configPath), is(false));
public ChangedTestClass(Path configPath) {super(configPath);}
cfg.save();
assertThat(Files.exists(configPath), is(true));
assertThat(ConfigReader.read(configPath), is(TestConfiguration.CONFIG_AS_TEXT));
}
}

@ -1,40 +0,0 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import org.junit.Before;
import org.junit.Test;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.Map;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class ConfigurationWriterTest {
private static final FileSystem fs = Jimfs.newFileSystem();
private Path configPath;
private ConfigurationWriter writer;
private String dump;
@Before
public void setUp() throws Exception {
configPath = fs.getPath("/config.yml");
FilteredFieldStreamSupplier streamSupplier = new FilteredFieldStreamSupplier(
TestConfiguration.class, ConfigurationFieldFilter.INSTANCE);
Comments comments = Comments.from(streamSupplier);
FieldMapper mapper = new FieldMapper(streamSupplier);
Map<String, Object> valuesByFieldNames = mapper.mapFieldNamesToValues(
new TestConfiguration(configPath));
dump = new YamlSerializer().serialize(valuesByFieldNames);
writer = new ConfigurationWriter(configPath, comments);
}
@Test
public void write() throws Exception {
writer.write(dump);
String read = new ConfigurationReader(configPath).read();
assertThat(read, is(TestConfiguration.CONFIG_AS_STRING));
}
}

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

@ -1,61 +1,133 @@
package de.exlll.configlib;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
public class FieldMapperTest {
@Rule
public ExpectedException exception = ExpectedException.none();
private static final FilteredFieldStreamSupplier streamSupplier =
new FilteredFieldStreamSupplier(TestClass.class, x -> true);
private static final FieldMapper mapper = new FieldMapper(streamSupplier);
@Test
public void instanceTopMapCreatesMap() throws Exception {
TestClass t = new TestClass();
Map<String, Object> map = FieldMapper.instanceToMap(t);
assertThat(map.get("i"), is(1));
assertThat(map.get("i"), instanceOf(Integer.class));
assertThat(map.get("z"), is(0));
assertThat(map.get("z"), instanceOf(Integer.class));
assertThat(map.get("d"), is(2.0));
assertThat(map.get("d"), instanceOf(Double.class));
assertThat(map.get("s"), is("s"));
assertThat(map.get("s"), instanceOf(String.class));
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));
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));
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
public void constructorRequiresNonNullSupplier() throws Exception {
exception.expect(NullPointerException.class);
new FieldMapper(null);
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"));
}
@Test
public void mapFieldNamesToValues() throws Exception {
TestClass testClass = new TestClass();
Map<String, Object> valuesByFieldNames = new LinkedHashMap<>();
public void instanceFromMapSetsValues() throws Exception {
TestClass t = new TestClass();
valuesByFieldNames.put("field1", "field1");
valuesByFieldNames.put("field2", 2);
valuesByFieldNames.put("field3", "field3");
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"));
assertThat(valuesByFieldNames, is(mapper.mapFieldNamesToValues(testClass)));
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);
Map<String, Object> bMap = new HashMap<>();
bMap.put("j", 20);
bMap.put("t", "v");
map.put("b", bMap);
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
public void mapValuesToFields() throws Exception {
Map<String, Object> valuesByFieldNames = new HashMap<>();
valuesByFieldNames.put("field1", "new");
valuesByFieldNames.put("field2", 10);
public void getValueGetsValue() throws Exception {
TestClass testClass = new TestClass();
Field s = TestClass.class.getDeclaredField("s");
assertThat(FieldMapper.getValue(s, testClass), is("s"));
}
@Test
public void setValueSetsValue() throws Exception {
TestClass testClass = new TestClass();
mapper.mapValuesToFields(valuesByFieldNames, testClass);
assertThat(testClass.field1, is("new"));
assertThat(testClass.field2, is(10));
assertThat(testClass.field3, is("field3"));
Field s = TestClass.class.getDeclaredField("s");
FieldMapper.setValue(s, testClass, "t");
assertThat(testClass.s, is("t"));
}
private static final class TestClass {
@Comment("Comment1")
private String field1 = "field1";
@Comment({"Comment2", "Comment3"})
private int field2 = 2;
private String field3 = "field3";
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();
public TestClass() {
objects.put("a", 1);
objects.put("b", 2);
}
}
private static final class TestClassB {
private int j = -1;
private String t = "t";
}
}

@ -1,73 +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 java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.*;
public class FilteredFieldStreamSupplierTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void constructorRequiresNonNullPredicate() throws Exception {
exception.expect(NullPointerException.class);
new FilteredFieldStreamSupplier(getClass(), null);
}
@Test
public void constructorRequiresNonNullClass() throws Exception {
exception.expect(NullPointerException.class);
new FilteredFieldStreamSupplier(null, field -> true);
}
@Test
public void supplierReturnsStream() throws Exception {
Supplier<Stream<Field>> supplier = new FilteredFieldStreamSupplier(
getClass(), field -> true);
Stream<Field> fieldStream = supplier.get();
assertThat(fieldStream, is(notNullValue()));
}
@Test
public void supplierApplysFilter() throws Exception {
Supplier<Stream<Field>> supplier = new FilteredFieldStreamSupplier(
TestClass.class, field -> !Modifier.isPublic(field.getModifiers()));
Stream<Field> fieldStream = supplier.get();
assertThat(fieldStream.count(), is(3L));
}
@Test
public void toListReturnsFieldsAsList() throws Exception {
FilteredFieldStreamSupplier supplier = new FilteredFieldStreamSupplier(
TestClass.class, field -> true);
List<Field> fields = supplier.toList();
assertThat(fields.get(0), is(TestClass.class.getDeclaredField("i")));
assertThat(fields.get(1), is(TestClass.class.getDeclaredField("j")));
assertThat(fields.get(2), is(TestClass.class.getDeclaredField("k")));
assertThat(fields.get(3), is(TestClass.class.getDeclaredField("l")));
}
private static final class TestClass {
public int i;
protected int j;
int k;
private int l;
}
}

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

@ -8,34 +8,6 @@ import java.util.*;
"This comment is applied to a class."
})
final class TestConfiguration extends Configuration {
static final String CONFIG_AS_STRING = "# 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";
@Comment({
"This comment is applied to a field.",
"It has more than 1 line."
@ -47,7 +19,7 @@ final class TestConfiguration extends Configuration {
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);
@ -113,32 +85,47 @@ final class TestConfiguration extends Configuration {
this.stringListsByString = stringListsByString;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TestConfiguration that = (TestConfiguration) o;
public Credentials getCredentials() {
return credentials;
}
if (port != that.port) return false;
if (Double.compare(that.modifier, modifier) != 0) return false;
if (!localhost.equals(that.localhost)) return false;
if (!allowedIps.equals(that.allowedIps)) return false;
if (!intsByStrings.equals(that.intsByStrings)) return false;
return stringListsByString.equals(that.stringListsByString);
public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}
@Override
public int hashCode() {
int result;
long temp;
result = port;
result = 31 * result + localhost.hashCode();
temp = Double.doubleToLongBits(modifier);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + allowedIps.hashCode();
result = 31 * result + intsByStrings.hashCode();
result = 31 * result + stringListsByString.hashCode();
return result;
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";
}

@ -1,119 +1,110 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.configs.CustomConfiguration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.yaml.snakeyaml.parser.ParserException;
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.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
public class YamlSerializerTest {
@Rule
public ExpectedException exception = ExpectedException.none();
@Test
public void deserializeRequiresValidYaml() throws Exception {
exception.expect(ParserException.class);
new YamlSerializer().deserialize("{a");
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 deserializeReturnsMaps() throws Exception {
Map<String, Object> actual = new YamlSerializer()
.deserialize("a: 1\nb: c");
Map<String, Object> expected = new HashMap<>();
expected.put("a", 1);
expected.put("b", "c");
assertThat(actual, is(expected));
public void constructorRequiresNonNullBaseConstructor() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(null, new Representer(), new DumperOptions(), new Resolver());
}
@Test
public void isDefaultClassReturnsOnlyTrueForDefaultClasses() throws Exception {
Class<?>[] classes = {
Boolean.class, Long.class, Integer.class, Short.class, Byte.class,
Double.class, Float.class, String.class, Character.class
};
assertThat(YamlSerializer.DEFAULT_CLASSES.size(), is(9));
YamlSerializer serializer = new YamlSerializer();
for (Class<?> cls : classes) {
assertThat(serializer.isDefaultClass(cls), is(true));
}
public void constructorRequiresNonNullRepresenter() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), new Representer(), null, new Resolver());
}
@Test
public void isDefaultInstanceChecksDefaultInstances() throws Exception {
YamlSerializer serializer = new YamlSerializer();
assertThat(serializer.isDefaultInstance(HashMap.class), is(true));
assertThat(serializer.isDefaultInstance(HashSet.class), is(true));
assertThat(serializer.isDefaultInstance(ArrayList.class), is(true));
assertThat(serializer.isDefaultInstance(Object.class), is(false));
public void constructorRequiresNonNullDumperOptions() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), null, new DumperOptions(), new Resolver());
}
@Test
public void tagAddedIfUnknown() throws Exception {
YamlSerializer serializer = new YamlSerializer();
Class<?> unknown = Unknown.class;
assertThat(serializer.isKnown(unknown), is(false));
serializer.addTagIfClassUnknown(unknown);
assertThat(serializer.isKnown(unknown), is(true));
public void constructorRequiresNonNullResolver() throws Exception {
expectedException.expect(NullPointerException.class);
new YamlSerializer(new Constructor(), new Representer(), new DumperOptions(), null);
}
@Test
public void serializeAddsUnknownTags() throws Exception {
YamlSerializer serializer = new YamlSerializer();
Class<?> unknown = Unknown.class;
assertThat(serializer.isKnown(unknown), is(false));
Map<String, Object> map = new HashMap<>();
serializer.serialize(map);
assertThat(serializer.isKnown(unknown), is(false));
map.put("", new Unknown());
serializer.serialize(map);
assertThat(serializer.isKnown(unknown), is(true));
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 serializeSerializesCustomObjects() throws Exception {
try (FileSystem system = Jimfs.newFileSystem()) {
Path path = system.getPath("/a");
CustomConfiguration saveConfig = new CustomConfiguration(path);
// cs1
saveConfig.getCs1().getMap().put("NEW KEY", Collections.singletonList("NEW VALUE"));
saveConfig.getCs1().getO1().setS1("NEW VALUE 1");
// cs2
saveConfig.getCs2().getO2().setS2("NEW VALUE 2");
saveConfig.getCs2().setI1(Integer.MAX_VALUE);
// config
saveConfig.setConfig("NEW CONFIG");
saveConfig.save();
CustomConfiguration loadConfig = new CustomConfiguration(path);
loadConfig.load();
assertThat(loadConfig.getCs1().getMap().get("NEW KEY"), hasItem("NEW VALUE"));
assertThat(loadConfig.getCs1().getO1().getS1(), is("NEW VALUE 1"));
assertThat(loadConfig.getCs2().getO2().getS2(), is("NEW VALUE 2"));
assertThat(loadConfig.getCs2().getI1(), is(Integer.MAX_VALUE));
assertThat(loadConfig.getConfig(), is("NEW CONFIG"));
}
}
public static final class Unknown {
public void deserialize() throws Exception {
assertThat(new YamlSerializer(
new Constructor(), new Representer(), dumperOptions, new Resolver()
).deserialize(serializedMap), is(map));
}
}

@ -1,39 +0,0 @@
package de.exlll.configlib.configs;
import de.exlll.configlib.Configuration;
import java.nio.file.Path;
public class CustomConfiguration extends Configuration {
private String config = "config1";
private CustomSection cs1 = new CustomSection();
private CustomSection cs2 = new CustomSection();
public CustomConfiguration(Path configPath) {
super(configPath);
}
public String getConfig() {
return config;
}
public void setConfig(String config1) {
this.config = config1;
}
public CustomSection getCs1() {
return cs1;
}
public void setCs1(CustomSection cs1) {
this.cs1 = cs1;
}
public CustomSection getCs2() {
return cs2;
}
public void setCs2(CustomSection cs2) {
this.cs2 = cs2;
}
}

@ -1,30 +0,0 @@
package de.exlll.configlib.configs;
public class CustomObject {
private String s1 = "s1";
private String s2 = "s2";
public String getS1() {
return s1;
}
public void setS1(String s1) {
this.s1 = s1;
}
public String getS2() {
return s2;
}
public void setS2(String s2) {
this.s2 = s2;
}
@Override
public String toString() {
return "CustomObject{" +
"s1='" + s1 + '\'' +
", s2='" + s2 + '\'' +
'}';
}
}

@ -1,71 +0,0 @@
package de.exlll.configlib.configs;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomSection {
private Map<String, List<String>> map = new HashMap<>();
private String s1 = "s1";
private int i1 = 1;
private CustomObject o1 = new CustomObject();
private CustomObject o2 = new CustomObject();
public CustomSection() {
map.put("xa", Arrays.asList("x1", "x2"));
map.put("ya", Arrays.asList("y1", "y2"));
map.put("za", Arrays.asList("z1", "z2"));
}
public Map<String, List<String>> getMap() {
return map;
}
public void setMap(Map<String, List<String>> map) {
this.map = map;
}
public String getS1() {
return s1;
}
public void setS1(String s1) {
this.s1 = s1;
}
public int getI1() {
return i1;
}
public void setI1(int i1) {
this.i1 = i1;
}
public CustomObject getO1() {
return o1;
}
public void setO1(CustomObject o1) {
this.o1 = o1;
}
public CustomObject getO2() {
return o2;
}
public void setO2(CustomObject o2) {
this.o2 = o2;
}
@Override
public String toString() {
return "CustomSection{" +
"map=" + map +
", s1='" + s1 + '\'' +
", i1=" + i1 +
", o1=" + o1 +
", o2=" + o2 +
'}';
}
}

@ -1,65 +1,68 @@
# 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 fields,
creating the configuration file and its parent directories if necessary.
by using Reflection on configuration classes and automatically saving and loading their
attribute values, creating the configuration file and its parent directories if necessary.
## 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 and its fields
- option to exclude fields by making them final, static or transient
- option to change the style of the configuration file
## General information
#### What can be serialized?
If your configuration class uses the following types as attributes, it will be properly saved.
- `String`s
- primitive types (e.g. `int`, `char`) and their corresponding wrapper types
(e.g. `Integer`, `Character`)
- `Set`s, `List`s, and `Map`s containing the above (e.g. `Set<Integer>`, `Map<String, Double>`)
- any other class that consists of the above
#### Default and null values
All reference type fields of a configuration class must be assigned non-null default values.
If a value is `null` while saving takes place, a `NullPointerException` will be thrown.
#### Serialization of custom classes
You can add fields to your configuration class whose type is some custom class. To be properly
serialized, the fields of the custom class need to be either public or have getter and setter
methods. `@Comment`s added to the custom class are ignored and won't be displayed in
the configuration file.
You can add fields to your configuration class whose type is some custom class.
`@Comment`s added to the custom class or its fields are ignored and won't be
displayed in the configuration file.
## How-to
##### Creating a configuration
#### Creating a configuration
To create a new configuration, create a class which extends `Configuration`. Fields which are
added to this class and which are not `final`, `static` or `transient` can automatically be saved
to the corresponding configuration file.
##### Saving and loading a configuration
#### Saving and loading a configuration
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.
##### Adding and removing fields
#### 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
#### Post load action
You can override `postLoadHook` to execute some action after the configuration has successfully
been loaded.
##### Comments
#### 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.
#### 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
#### Example of a custom class
```java
public class Custom {
public String publicString = "public"; // doesn't need any additional methods to be saved
private String privateString = "private"; // needs additional getter and setter methods
public String getPrivateString() {
return privateString;
}
public void setPrivateString(String privateString) {
this.privateString = privateString;
}
public class Credentials {
private String username = "minecraft";
private String password = "secret";
}
```
#### Example database configuration
@ -87,12 +90,12 @@ public final class DatabaseConfig extends Configuration {
"It describes what this field does."
})
private Map<String, List<String>> listByStrings = new HashMap<>();
private Custom custom = new Custom();
private Credentials credentials = new Credentials();
public DatabaseConfig(Path configPath) {
super(configPath);
}
/* optional GETTER and SETTER methods */
/* other methods */
}
```
#### Example Bukkit plugin
@ -113,3 +116,47 @@ public class ExamplePlugin extends JavaPlugin {
}
}
```
## Import
#### Maven
```xml
<repository>
<id>de.exlll</id>
<url>https://repo.exlll.de/artifactory/releases/</url>
</repository>
<!-- for Bukkit plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bukkit</artifactId>
<version>1.2.0</version>
</dependency>
<!-- for Bungee plugins -->
<dependency>
<groupId>de.exlll</groupId>
<artifactId>configlib-bungee</artifactId>
<version>1.2.0</version>
</dependency>
```
#### Gradle
```groovy
repositories {
maven {
url 'https://repo.exlll.de/artifactory/releases/'
}
}
dependencies {
// for Bukkit plugins
compile group: 'de.exlll', name: 'configlib-bukkit', version: '1.2.0'
// for Bungee plugins
compile group: 'de.exlll', name: 'configlib-bungee', version: '1.2.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.1.0'
version '1.2.0'
}
subprojects {
apply plugin: 'java'

Loading…
Cancel
Save