Implement serialization.

This provides functionality to convert populated Toml instances
and arbitrary objects into TOML.
This commit is contained in:
Jonathan Wood 2015-05-31 21:08:42 -07:00
parent 5402ce22f3
commit e7d7de7ae5
15 changed files with 863 additions and 16 deletions

View file

@ -204,6 +204,34 @@ Long tableD = toml.getLong("table.d"); // returns null, not 5, because of shallo
Long arrayD = toml.getLong("array[0].d"); // returns 3
```
### Serialization
You can serialize a `Toml` or any arbitrary object to a TOML string.
Once you have populated a `Toml` via `Toml.parse()`, you can serialize it back to TOML.
```java
Toml toml = new Toml().parse("a=1");
String tomlString = toml.serialize();
```
Or you can serialize any object.
```java
class AClass {
int anInt = 1;
int[] anArray = { 2, 3 };
}
String tomlString = Toml.serializeFrom(new AClass());
/*
yields:
anInt = 1
anArray = [ 2, 3 ]
*/
```
### Limitations
Date precision is limited to milliseconds.

View file

@ -0,0 +1,69 @@
package com.moandjiezana.toml;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
abstract class ArraySerializer implements Serializer {
static protected boolean isArrayish(Object value) {
return value instanceof Collection || value.getClass().isArray();
}
@Override
public boolean isPrimitiveType() {
return false;
}
@Override
public boolean isTable() {
return false;
}
static boolean isArrayOfPrimitive(Object array) {
Object first = peek(array);
if (first != null) {
Serializer serializer = Serializers.findSerializerFor(first);
return serializer.isPrimitiveType() || isArrayish(first);
}
return false;
}
@SuppressWarnings("unchecked")
protected Collection normalize(Object value) {
Collection collection;
if (value.getClass().isArray()) {
// Arrays.asList() interprets an array as a single element,
// so convert it to a list by hand
collection = new ArrayList<Object>(Array.getLength(value));
for (int i = 0; i < Array.getLength(value); i++) {
Object elem = Array.get(value, i);
collection.add(elem);
}
} else {
collection = (Collection) value;
}
return collection;
}
@SuppressWarnings("unchecked")
private static Object peek(Object value) {
if (value.getClass().isArray()) {
if (Array.getLength(value) > 0) {
return Array.get(value, 0);
} else {
return null;
}
} else {
Collection collection = (Collection) value;
if (collection.size() > 0) {
return collection.iterator().next();
}
}
return null;
}
}

View file

@ -0,0 +1,25 @@
package com.moandjiezana.toml;
class BooleanSerializer implements Serializer {
static final Serializer BOOLEAN_SERIALIZER = new BooleanSerializer();
@Override
public boolean canSerialize(Object value) {
return Boolean.class.isInstance(value);
}
@Override
public void serialize(Object value, SerializerContext context) {
context.serialized.append(value.toString());
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
}

View file

@ -0,0 +1,29 @@
package com.moandjiezana.toml;
import java.text.SimpleDateFormat;
import java.util.Date;
class DateSerializer implements Serializer {
static final Serializer DATE_SERIALIZER = new DateSerializer();
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:m:ssXXX");
@Override
public boolean canSerialize(Object value) {
return value instanceof Date;
}
@Override
public void serialize(Object value, SerializerContext context) {
context.serialized.append(dateFormat.format(value));
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
}

View file

@ -0,0 +1,98 @@
package com.moandjiezana.toml;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.moandjiezana.toml.PrimitiveArraySerializer.PRIMITIVE_ARRAY_SERIALIZER;
import static com.moandjiezana.toml.TableArraySerializer.TABLE_ARRAY_SERIALIZER;
class MapSerializer implements Serializer {
static final Serializer MAP_SERIALIZER = new MapSerializer();
private static final Pattern requiredQuotingPattern = Pattern.compile("^.*[^A-Za-z\\d_-].*$");
@Override
public boolean canSerialize(Object value) {
return value instanceof Map;
}
@Override
public void serialize(Object value, SerializerContext context) {
Map from = (Map) value;
if (hasPrimitiveValues(from)) {
context.serializeKey();
}
// Render primitive types and arrays of primitive first so they are
// grouped under the same table (if there is one)
for (Object key : from.keySet()) {
Object fromValue = from.get(key);
if (fromValue == null) {
continue;
}
Serializer serializer = Serializers.findSerializerFor(fromValue);
if (serializer.isPrimitiveType()) {
context.indent();
context.serialized.append(quoteKey(key)).append(" = ");
serializer.serialize(fromValue, context);
context.serialized.append('\n');
} else if (serializer == PRIMITIVE_ARRAY_SERIALIZER) {
context.serialized.append(quoteKey(key)).append(" = ");
serializer.serialize(fromValue, context);
context.serialized.append('\n');
}
}
// Now render (sub)tables and arrays of tables
for (Object key : from.keySet()) {
Object fromValue = from.get(key);
if (fromValue == null) {
continue;
}
Serializer serializer = Serializers.findSerializerFor(fromValue);
if (serializer.isTable() || serializer == TABLE_ARRAY_SERIALIZER) {
serializer.serialize(fromValue, context.extend(quoteKey(key)));
}
}
}
@Override
public boolean isPrimitiveType() {
return false;
}
@Override
public boolean isTable() {
return true;
}
private static String quoteKey(Object key) {
String stringKey = key.toString();
Matcher matcher = requiredQuotingPattern.matcher(stringKey);
if (matcher.matches()) {
stringKey = "\"" + stringKey + "\"";
}
return stringKey;
}
private static boolean hasPrimitiveValues(Map values) {
for (Object key : values.keySet()) {
Object fromValue = values.get(key);
if (fromValue == null) {
continue;
}
Serializer serializer = Serializers.findSerializerFor(fromValue);
if (serializer.isPrimitiveType() || serializer == PRIMITIVE_ARRAY_SERIALIZER) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,25 @@
package com.moandjiezana.toml;
class NumberSerializer implements Serializer {
static final Serializer NUMBER_SERIALIZER = new NumberSerializer();
@Override
public boolean canSerialize(Object value) {
return Number.class.isInstance(value);
}
@Override
public void serialize(Object value, SerializerContext context) {
context.serialized.append(value.toString());
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
}

View file

@ -0,0 +1,76 @@
package com.moandjiezana.toml;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.function.Predicate;
import static com.moandjiezana.toml.MapSerializer.MAP_SERIALIZER;
class ObjectSerializer implements Serializer {
static final Serializer OBJECT_SERIALIZER = new ObjectSerializer();
@Override
public boolean canSerialize(Object value) {
return true;
}
@Override
public void serialize(Object value, SerializerContext context) {
Map<String, Object> to = new LinkedHashMap<String, Object>();
Set<Field> fields = getFieldsForClass(value.getClass());
for (Field field : fields) {
to.put(field.getName(), getFieldValue(field, value));
}
MAP_SERIALIZER.serialize(to, context);
}
@Override
public boolean isPrimitiveType() {
return false;
}
@Override
public boolean isTable() {
return true;
}
static private Set<Field> getFieldsForClass(Class cls) {
Set<Field> fields = new LinkedHashSet<Field>(Arrays.asList(cls.getDeclaredFields()));
getSuperClassFields(cls.getSuperclass(), fields);
// Skip final fields
fields.removeIf(new Predicate<Field>() {
@Override
public boolean test(Field field) {
return Modifier.isFinal(field.getModifiers());
}
});
return fields;
}
static private void getSuperClassFields(Class cls, Set<Field> fields) {
if (cls == Object.class) {
return;
}
fields.addAll(Arrays.asList(cls.getDeclaredFields()));
getSuperClassFields(cls.getSuperclass(), fields);
}
static private Object getFieldValue(Field field, Object o) {
boolean isAccessible = field.isAccessible();
field.setAccessible(true);
Object value = null;
try {
value = field.get(o);
} catch (IllegalAccessException ignored) {
}
field.setAccessible(isAccessible);
return value;
}
}

View file

@ -0,0 +1,28 @@
package com.moandjiezana.toml;
import java.util.Collection;
class PrimitiveArraySerializer extends ArraySerializer {
static final Serializer PRIMITIVE_ARRAY_SERIALIZER = new PrimitiveArraySerializer();
@Override
public boolean canSerialize(Object value) {
return isArrayish(value) && isArrayOfPrimitive(value);
}
@Override
public void serialize(Object value, SerializerContext context) {
Collection values = normalize(value);
context.serialized.append("[ ");
boolean first = true;
for (Object elem : values) {
if (!first) {
context.serialized.append(", ");
}
Serializers.serialize(elem, context);
first = false;
}
context.serialized.append(" ]");
}
}

View file

@ -0,0 +1,11 @@
package com.moandjiezana.toml;
interface Serializer {
boolean canSerialize(Object value);
void serialize(Object value, SerializerContext context);
boolean isPrimitiveType();
boolean isTable();
}

View file

@ -0,0 +1,50 @@
package com.moandjiezana.toml;
class SerializerContext {
private String key = "";
private boolean isArrayOfTable = false;
StringBuilder serialized = new StringBuilder();
SerializerContext(String key, StringBuilder serialized) {
this.key = key;
this.serialized = serialized;
}
SerializerContext() {
}
SerializerContext extend(String newKey) {
String fullKey = key + (key.isEmpty() ? newKey : "." + newKey);
return new SerializerContext(fullKey, serialized);
}
SerializerContext extend() {
return new SerializerContext(key, serialized);
}
void serializeKey() {
if (key.isEmpty()) {
return;
}
if (serialized.length() > 0) {
serialized.append('\n');
}
if (isArrayOfTable) {
serialized.append("[[").append(key).append("]]\n");
} else {
serialized.append('[').append(key).append("]\n");
}
}
void indent() {
serialized.append(key.isEmpty() ? "" : " ");
}
SerializerContext setIsArrayOfTable(boolean isArrayOfTable) {
this.isArrayOfTable = isArrayOfTable;
return this;
}
}

View file

@ -0,0 +1,38 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.BooleanSerializer.BOOLEAN_SERIALIZER;
import static com.moandjiezana.toml.DateSerializer.DATE_SERIALIZER;
import static com.moandjiezana.toml.MapSerializer.MAP_SERIALIZER;
import static com.moandjiezana.toml.NumberSerializer.NUMBER_SERIALIZER;
import static com.moandjiezana.toml.ObjectSerializer.OBJECT_SERIALIZER;
import static com.moandjiezana.toml.PrimitiveArraySerializer.PRIMITIVE_ARRAY_SERIALIZER;
import static com.moandjiezana.toml.StringSerializer.STRING_SERIALIZER;
import static com.moandjiezana.toml.TableArraySerializer.TABLE_ARRAY_SERIALIZER;
abstract class Serializers {
private static final Serializer[] SERIALIZERS = {
STRING_SERIALIZER, NUMBER_SERIALIZER, BOOLEAN_SERIALIZER, DATE_SERIALIZER,
MAP_SERIALIZER, PRIMITIVE_ARRAY_SERIALIZER, TABLE_ARRAY_SERIALIZER
};
static Serializer findSerializerFor(Object value) {
for (Serializer serializer : SERIALIZERS) {
if (serializer.canSerialize(value)) {
return serializer;
}
}
return OBJECT_SERIALIZER;
}
static String serialize(Object value) {
SerializerContext context = new SerializerContext();
serialize(value, context);
return context.serialized.toString();
}
static void serialize(Object value, SerializerContext context) {
findSerializerFor(value).serialize(value, context);
}
}

View file

@ -0,0 +1,56 @@
package com.moandjiezana.toml;
class StringSerializer implements Serializer {
static final Serializer STRING_SERIALIZER = new StringSerializer();
static private final String[] specialCharacterEscapes = new String[93];
static {
specialCharacterEscapes[0x08] = "\\b";
specialCharacterEscapes[0x09] = "\\t";
specialCharacterEscapes[0x0A] = "\\n";
specialCharacterEscapes[0x0C] = "\\f";
specialCharacterEscapes[0x0D] = "\\r";
specialCharacterEscapes[0x22] = "\\\"";
specialCharacterEscapes[0x5C] = "\\";
}
@Override
public boolean canSerialize(Object value) {
return value.getClass().isAssignableFrom(String.class);
}
@Override
public void serialize(Object value, SerializerContext context) {
context.serialized.append('"');
escapeUnicode(value.toString(), context.serialized);
context.serialized.append('"');
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
private void escapeUnicode(String in, StringBuilder out) {
for (int i = 0; i < in.length(); i++) {
int codePoint = in.codePointAt(i);
if (codePoint < specialCharacterEscapes.length && specialCharacterEscapes[codePoint] != null) {
out.append(specialCharacterEscapes[codePoint]);
} else if (codePoint > 0x1f && codePoint < 0x7f) {
out.append(Character.toChars(codePoint));
} else if (codePoint <= 0xFFFF) {
out.append(String.format("\\u%04X", codePoint));
} else {
out.append(String.format("\\U%08X", codePoint));
// Skip the low surrogate, which will be the next in the code point sequence.
i++;
}
}
}
}

View file

@ -0,0 +1,23 @@
package com.moandjiezana.toml;
import java.util.Collection;
class TableArraySerializer extends ArraySerializer {
static final Serializer TABLE_ARRAY_SERIALIZER = new TableArraySerializer();
@Override
public boolean canSerialize(Object value) {
return isArrayish(value) && !isArrayOfPrimitive(value);
}
@Override
public void serialize(Object value, SerializerContext context) {
Collection values = normalize(value);
SerializerContext subContext = context.extend().setIsArrayOfTable(true);
for (Object elem : values) {
Serializers.serialize(elem, subContext);
}
}
}

View file

@ -1,24 +1,11 @@
package com.moandjiezana.toml;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import java.io.*;
import java.util.*;
/**
* <p>Provides access to the keys and tables in a TOML data source.</p>
*
@ -282,6 +269,29 @@ public class Toml {
return DEFAULT_GSON.fromJson(json, targetClass);
}
/**
* Serializes the values of this Toml instance into TOML.
*
* @return a string containing the TOML representation of this Toml instance.
*/
public String serialize() {
return Serializers.serialize(values);
}
/**
* Serializes an Object into TOML.
*
* The input can comprise arbitrarily nested combinations of Java primitive types,
* other {@link Object}s, {@link Map}s, {@link List}s, and Arrays. {@link Object}s and {@link Map}s
* are serialized to TOML tables, and {@link List}s and Array to TOML arrays.
*
* @param from the object to be serialized
* @return a string containing the TOML representation of the given Object
*/
public static String serializeFrom(Object from) {
return Serializers.serialize(from);
}
@SuppressWarnings("unchecked")
private Object get(String key) {
if (values.containsKey(key)) {

View file

@ -0,0 +1,281 @@
package com.moandjiezana.toml;
import org.junit.Test;
import java.io.UnsupportedEncodingException;
import java.util.*;
import static org.junit.Assert.assertEquals;
public class SerializerTest {
@Test
public void serializesPrimitiveTypes() {
class TestClass {
public String aString;
int anInt;
protected float aFloat;
private double aDouble;
boolean aBoolean;
final int aFinalInt = 1; // Should be skipped
Date aDate;
}
TestClass o = new TestClass();
o.aString = "hello";
o.anInt = 4;
o.aFloat = 1.23f;
o.aDouble = -5.43;
o.aBoolean = false;
String theDate = "2015-05-31T08:44:03-07:00";
Toml dateToml = new Toml().parse("a_date = " + theDate);
o.aDate = dateToml.getDate("a_date");
String serialized = Toml.serializeFrom(o);
String expected = "aString = \"hello\"\n" +
"anInt = 4\n" +
"aFloat = 1.23\n" +
"aDouble = -5.43\n" +
"aBoolean = false\n" +
"aDate = " + theDate + "\n";
assertEquals(expected, serialized);
}
@Test
public void serializesNestedMap() {
class SubChild {
int anInt;
}
class Child {
SubChild subChild;
int anInt;
}
class Parent {
Map<String, Object> aMap;
Child child;
boolean aBoolean;
}
Parent parent = new Parent();
parent.aMap = new LinkedHashMap<String, Object>();
parent.aMap.put("foo", 1);
parent.aMap.put("bar", "value1");
parent.aMap.put("baz.x", true);
parent.child = new Child();
parent.child.anInt = 2;
parent.child.subChild = new SubChild();
parent.child.subChild.anInt = 4;
parent.aBoolean = true;
String serialized = Toml.serializeFrom(parent);
String expected = "aBoolean = true\n\n" +
"[aMap]\n" +
" foo = 1\n" +
" bar = \"value1\"\n" +
" \"baz.x\" = true\n\n" +
"[child]\n" +
" anInt = 2\n\n" +
"[child.subChild]\n" +
" anInt = 4\n";
assertEquals(expected, serialized);
}
@Test
public void serializesArrayOfPrimitive() {
class ArrayTest {
int[] array = {1, 2, 3};
}
ArrayTest arrayTest = new ArrayTest();
String serialized = Toml.serializeFrom(arrayTest);
String expected = "array = [ 1, 2, 3 ]\n";
assertEquals(expected, serialized);
}
@Test
public void serializesArrayOfTables() {
class Table {
int anInt;
Table(int anInt) {
this.anInt = anInt;
}
}
class Config {
Table[] table;
}
Config config = new Config();
config.table = new Table[]{new Table(1), new Table(2)};
String serialized = Toml.serializeFrom(config);
String expected = "[[table]]\n" +
" anInt = 1\n\n" +
"[[table]]\n" +
" anInt = 2\n";
assertEquals(expected, serialized);
}
@Test
public void serializesArrayOfArray() {
class ArrayTest {
int[][] array = {{1, 2, 3}, {4, 5, 6}};
}
ArrayTest arrayTest = new ArrayTest();
String serialized = Toml.serializeFrom(arrayTest);
String expected = "array = [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]\n";
assertEquals(expected, serialized);
}
@Test
public void serializesList() {
class ListTest {
List<Integer> aList = new LinkedList<Integer>();
}
ListTest o = new ListTest();
o.aList.add(1);
o.aList.add(2);
assertEquals("aList = [ 1, 2 ]\n", Toml.serializeFrom(o));
}
@Test
public void handlesZeroLengthArraysAndLists() {
class TestClass {
List<Integer> aList = new LinkedList<Integer>();
Float[] anArray = new Float[0];
}
assertEquals("", Toml.serializeFrom(new TestClass()));
}
@Test
public void elidesEmptyIntermediateTables() {
class C {
int anInt = 1;
}
class B {
C c = new C();
}
class A {
B b = new B();
}
assertEquals("[b.c]\n anInt = 1\n", Toml.serializeFrom(new A()));
}
@Test
public void serializesNestedArraysOfTables() {
class Physical {
String color;
String shape;
}
class Variety {
String name;
}
class Fruit {
Physical physical;
Variety[] variety;
String name;
}
class Basket {
Fruit[] fruit;
}
Basket basket = new Basket();
basket.fruit = new Fruit[2];
basket.fruit[0] = new Fruit();
basket.fruit[0].name = "apple";
basket.fruit[0].physical = new Physical();
basket.fruit[0].physical.color = "red";
basket.fruit[0].physical.shape = "round";
basket.fruit[0].variety = new Variety[2];
basket.fruit[0].variety[0] = new Variety();
basket.fruit[0].variety[0].name = "red delicious";
basket.fruit[0].variety[1] = new Variety();
basket.fruit[0].variety[1].name = "granny smith";
basket.fruit[1] = new Fruit();
basket.fruit[1].name = "banana";
basket.fruit[1].variety = new Variety[1];
basket.fruit[1].variety[0] = new Variety();
basket.fruit[1].variety[0].name = "plantain";
String expected = "[[fruit]]\n" +
" name = \"apple\"\n" +
"\n" +
"[fruit.physical]\n" +
" color = \"red\"\n" +
" shape = \"round\"\n" +
"\n" +
"[[fruit.variety]]\n" +
" name = \"red delicious\"\n" +
"\n" +
"[[fruit.variety]]\n" +
" name = \"granny smith\"\n" +
"\n" +
"[[fruit]]\n" +
" name = \"banana\"\n" +
"\n" +
"[[fruit.variety]]\n" +
" name = \"plantain\"" +
"\n";
String serialized = Toml.serializeFrom(basket);
assertEquals(expected, serialized);
}
@Test
public void serializesClassesWithInheritance() {
class Parent {
protected int anInt = 2;
}
class Child extends Parent {
boolean aBoolean = true;
}
Child child = new Child();
String expected = "aBoolean = true\nanInt = 2\n";
assertEquals(expected, Toml.serializeFrom(child));
}
@Test
public void emptyTomlSerializesToEmptyString() {
Toml toml = new Toml();
assertEquals("", toml.serialize());
}
@Test
public void serializesStringsToTomlUtf8() throws UnsupportedEncodingException {
String input = " é foo € \b \t \n \f \r \" \\ ";
assertEquals("\" \\u00E9 foo \\u20AC \\b \\t \\n \\f \\r \\\" \\ \"", Toml.serializeFrom(input));
// Check unicode code points greater than 0XFFFF
input = " \uD801\uDC28 \uD840\uDC0B ";
assertEquals("\" \\U00010428 \\U0002000B \"", Toml.serializeFrom(input));
}
@Test
public void quotesKeys() {
Map<String, Integer> aMap = new LinkedHashMap<String, Integer>();
aMap.put("a.b", 1);
aMap.put("5€", 2);
aMap.put("c$d", 3);
aMap.put("e/f", 4);
String expected = "\"a.b\" = 1\n" +
"\"5€\" = 2\n" +
"\"c$d\" = 3\n" +
"\"e/f\" = 4\n";
assertEquals(expected, Toml.serializeFrom(aMap));
}
@Test
public void serializesFromToml() {
String tomlString = "a = 1\n";
Toml toml = new Toml().parse(tomlString);
assertEquals(tomlString, toml.serialize());
}
}