serialize custom classes

dev
Exlll 8 years ago
parent 5352d4183c
commit 0dca3f614f

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

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

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

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

@ -16,7 +16,7 @@ final class Comments {
this.commentsByFieldNames = commentsByFieldName;
}
public static Comments from(FilteredFieldStreamSupplier supplier) {
static Comments from(FilteredFieldStreamSupplier supplier) {
Objects.requireNonNull(supplier);
List<String> classComments = getComments(supplier.getSupplyingClass());
@ -27,22 +27,22 @@ final class Comments {
return new Comments(classComments, commentsByFieldNames);
}
public static List<String> getComments(AnnotatedElement element) {
static List<String> getComments(AnnotatedElement element) {
Comment comment = element.getAnnotation(Comment.class);
return (comment != null) ?
Arrays.asList(comment.value()) :
Collections.emptyList();
}
public static boolean hasCommentAnnotation(AnnotatedElement element) {
static boolean hasCommentAnnotation(AnnotatedElement element) {
return element.isAnnotationPresent(Comment.class);
}
public List<String> getClassComments() {
List<String> getClassComments() {
return classComments;
}
public Map<String, List<String>> getCommentsByFieldNames() {
Map<String, List<String>> getCommentsByFieldNames() {
return commentsByFieldNames;
}
}

@ -2,7 +2,6 @@ package de.exlll.configlib;
import org.yaml.snakeyaml.parser.ParserException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
@ -14,11 +13,13 @@ public abstract class Configuration {
private final Path configPath;
private final FieldMapper fieldMapper;
private final ConfigurationWriter writer;
private final YamlSerializer serializer;
/**
* Creates a new {@code Configuration} instance.
* <p>
* You can use {@link File#toPath()} to obtain a {@link Path} object from a {@link File}.
* You can use {@link java.io.File#toPath()} to obtain a {@link Path} object
* from a {@link java.io.File}.
*
* @param configPath location of the configuration file
* @throws NullPointerException if {@code configPath} is null
@ -33,6 +34,8 @@ public abstract class Configuration {
this.configPath = configPath;
this.fieldMapper = new FieldMapper(ffss);
this.writer = new ConfigurationWriter(configPath, comments);
this.serializer = new YamlSerializer();
ffss.fieldTypes().forEach(serializer::addTagIfClassUnknown);
}
/**
@ -44,7 +47,7 @@ public abstract class Configuration {
*/
public final void load() throws IOException {
String dump = new ConfigurationReader(configPath).read();
Map<String, Object> valuesByFieldNames = YamlSerializer.deserialize(dump);
Map<String, Object> valuesByFieldNames = serializer.deserialize(dump);
fieldMapper.mapValuesToFields(valuesByFieldNames, this);
postLoadHook();
}
@ -63,7 +66,7 @@ public abstract class Configuration {
createParentDirectories();
Map<String, Object> valuesByFieldNames = fieldMapper.mapFieldNamesToValues(this);
String dump = YamlSerializer.serialize(valuesByFieldNames);
String dump = serializer.serialize(valuesByFieldNames);
writer.write(dump);
}
@ -87,7 +90,12 @@ public abstract class Configuration {
}
/**
* Can be overridden to do something after all fields have been loaded.
* Protected method invoked after all fields have been loaded.
* <p>
* The default implementation of this method does nothing.
* <p>
* Subclasses may override this method in order to execute some action
* after all fields have been loaded.
*/
protected void postLoadHook() {
}

@ -5,14 +5,14 @@ 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);
}
};
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);
}
}

@ -18,14 +18,16 @@ final class FieldMapper {
Map<String, Object> mapFieldNamesToValues(Object instance) {
return streamSupplier.get().collect(
toMap(Field::getName,
field -> getValue(field, instance),
(f1, f2) -> f1,
LinkedHashMap::new));
field -> getValue(field, instance),
(f1, f2) -> f1,
LinkedHashMap::new));
}
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 */
@ -33,6 +35,14 @@ final class FieldMapper {
}
}
private 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);
throw new NullPointerException(msg);
}
}
void mapValuesToFields(Map<String, Object> valuesByFieldNames, Object instance) {
for (Field field : streamSupplier.toList()) {
String fieldName = field.getName();

@ -26,6 +26,11 @@ final class FilteredFieldStreamSupplier implements Supplier<Stream<Field>> {
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());
}

@ -1,23 +1,36 @@
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.parser.ParserException;
import org.yaml.snakeyaml.representer.Representer;
import java.util.Map;
import java.util.*;
enum YamlSerializer {
;
private static final Yaml yaml = createYaml();
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 static Yaml createYaml() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setIndent(2);
return new Yaml(options);
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);
}
static String serialize(Map<String, Object> mapToDump) {
String serialize(Map<String, Object> mapToDump) {
mapToDump.forEach((fieldName, fieldValue) -> addTagIfClassUnknown(fieldValue.getClass()));
return yaml.dump(mapToDump);
}
@ -25,9 +38,31 @@ enum YamlSerializer {
* @throws ParserException if invalid YAML
* @throws ClassCastException if parsed Object is not a {@code Map}
*/
static Map<String, Object> deserialize(String stringToLoad) {
Map<String, Object> deserialize(String stringToLoad) {
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>) yaml.load(stringToLoad);
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);
}
}

@ -26,7 +26,7 @@ public class ConfigurationWriterTest {
FieldMapper mapper = new FieldMapper(streamSupplier);
Map<String, Object> valuesByFieldNames = mapper.mapFieldNamesToValues(
new TestConfiguration(configPath));
dump = YamlSerializer.serialize(valuesByFieldNames);
dump = new YamlSerializer().serialize(valuesByFieldNames);
writer = new ConfigurationWriter(configPath, comments);
}

@ -1,13 +1,17 @@
package de.exlll.configlib;
import com.google.common.jimfs.Jimfs;
import de.exlll.configlib.configs.CustomConfiguration;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.yaml.snakeyaml.parser.ParserException;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.*;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@ -16,14 +20,14 @@ public class YamlSerializerTest {
public ExpectedException exception = ExpectedException.none();
@Test
public void desirializeRequiresValidYaml() throws Exception {
public void deserializeRequiresValidYaml() throws Exception {
exception.expect(ParserException.class);
YamlSerializer.deserialize("{a");
new YamlSerializer().deserialize("{a");
}
@Test
public void desirializeReturnsMaps() throws Exception {
Map<String, Object> actual = YamlSerializer
public void deserializeReturnsMaps() throws Exception {
Map<String, Object> actual = new YamlSerializer()
.deserialize("a: 1\nb: c");
Map<String, Object> expected = new HashMap<>();
@ -32,4 +36,84 @@ public class YamlSerializerTest {
assertThat(actual, is(expected));
}
@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));
}
}
@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));
}
@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));
}
@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));
}
@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 {
}
}

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

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

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

@ -9,6 +9,16 @@ This library facilitates creating, saving and loading YAML configuration files.
- 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
## General information
#### 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.
## How-to
##### Creating a configuration
To create a new configuration, create a class which extends `Configuration`. Fields which are
@ -18,9 +28,9 @@ added to this class and which are not `final`, `static` or `transient` can autom
##### 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
- `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
- `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
@ -28,11 +38,31 @@ In order to add or to remove fields, you just need to add them to or remove them
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` is written into a new line.
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.
## 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
#### Example class
public String getPrivateString() {
return privateString;
}
public void setPrivateString(String privateString) {
this.privateString = privateString;
}
}
```
#### Example database configuration
```java
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
@ -57,6 +87,7 @@ 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();
public DatabaseConfig(Path configPath) {
super(configPath);

@ -1,6 +1,6 @@
allprojects {
group 'de.exlll'
version '1.0.0'
version '1.1.0'
}
subprojects {
apply plugin: 'java'

Loading…
Cancel
Save