Merge branch 'wip' of git://github.com/dilecti/toml4j into writer

This commit is contained in:
moandji.ezana 2015-06-29 08:30:43 +02:00
commit 121db7ef05
21 changed files with 1374 additions and 43 deletions

View file

@ -9,6 +9,7 @@
### Added
* Support for writing objects to TOML format.
* Support for underscores in numbers (the feature branch had accidentally not been merged! :( )
* Set<Map.Entry> Toml#entrySet() cf. Reflection section in README
* Overloaded getters that take a default value. Thanks to __[udiabon](https://github.com/udiabon)__.

View file

@ -233,6 +233,35 @@ toml.containsTable("a"); // false
toml.containsTableArray("a"); // false
```
### Converting Objects To TOML
You can write any arbitrary object to a TOML `String`, `File`, `Writer`, or `OutputStream`.
```java
class AClass {
int anInt = 1;
int[] anArray = { 2, 3 };
}
String tomlString = new TomlWriter().write(new AClass());
/*
yields:
anInt = 1
anArray = [ 2, 3 ]
*/
```
You can exert some control over formatting. For example, you can change the default indentation, white space, and time zone:
```java
new TomlWriter().
wantTerseArrays(true).
setIndentationPolicy(new WriterIndentationPolicy().setTableIndent(2)).
setTimeZone(TimeZone.getTimeZone("UTC"));
```
See the `TomlWriter` class for more details.
### Limitations

View file

@ -0,0 +1,71 @@
package com.moandjiezana.toml;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import static com.moandjiezana.toml.ValueWriters.WRITERS;
abstract class ArrayValueWriter implements ValueWriter {
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) {
ValueWriter valueWriter = WRITERS.findWriterFor(first);
return valueWriter.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

@ -3,7 +3,7 @@ package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
class BooleanConverter implements ValueConverter {
class BooleanConverter implements ValueConverter, ValueWriter {
static final BooleanConverter BOOLEAN_PARSER = new BooleanConverter();
@ -24,5 +24,30 @@ class BooleanConverter implements ValueConverter {
return b;
}
@Override
public boolean canWrite(Object value) {
return Boolean.class.isInstance(value);
}
@Override
public void write(Object value, WriterContext context) {
context.output.append(value.toString());
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
private BooleanConverter() {}
@Override
public String toString() {
return "boolean";
}
}

View file

@ -1,11 +1,14 @@
package com.moandjiezana.toml;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class DateConverter implements ValueConverter {
class DateConverter implements ValueConverter, ValueWriter {
static final DateConverter DATE_PARSER = new DateConverter();
private static final Pattern DATE_REGEX = Pattern.compile("(\\d{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9])(\\.\\d*)?(Z|(?:[+\\-]\\d{2}:\\d{2}))(.*)");
@ -79,6 +82,46 @@ class DateConverter implements ValueConverter {
return errors;
}
}
@Override
public boolean canWrite(Object value) {
return value instanceof Date;
}
@Override
public void write(Object value, WriterContext context) {
DateFormat dateFormat;
DateFormat customDateFormat = context.getTomlWriter().getDateFormat();
if (customDateFormat != null) {
dateFormat = customDateFormat;
} else {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:m:ss");
}
dateFormat.setTimeZone(context.getTomlWriter().getTimeZone());
context.output.append(dateFormat.format(value));
if (customDateFormat == null) {
Calendar calendar = context.getTomlWriter().getCalendar();
int tzOffset = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
context.output.append(String.format("%+03d:%02d", tzOffset / 60, tzOffset % 60));
}
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
private DateConverter() {}
@Override
public String toString() {
return "datetime";
}
}

View file

@ -0,0 +1,102 @@
package com.moandjiezana.toml;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.moandjiezana.toml.PrimitiveArrayValueWriter.PRIMITIVE_ARRAY_VALUE_WRITER;
import static com.moandjiezana.toml.TableArrayValueWriter.TABLE_ARRAY_VALUE_WRITER;
import static com.moandjiezana.toml.ValueWriters.WRITERS;
class MapValueWriter implements ValueWriter {
static final ValueWriter MAP_VALUE_WRITER = new MapValueWriter();
private static final Pattern requiredQuotingPattern = Pattern.compile("^.*[^A-Za-z\\d_-].*$");
@Override
public boolean canWrite(Object value) {
return value instanceof Map;
}
@Override
public void write(Object value, WriterContext context) {
Map from = (Map) value;
if (hasPrimitiveValues(from, context)) {
context.writeKey();
}
// 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;
}
ValueWriter valueWriter = WRITERS.findWriterFor(fromValue);
if (valueWriter.isPrimitiveType()) {
context.indent();
context.output.append(quoteKey(key)).append(" = ");
valueWriter.write(fromValue, context);
context.output.append('\n');
} else if (valueWriter == PRIMITIVE_ARRAY_VALUE_WRITER) {
context.setArrayKey(key.toString());
context.output.append(quoteKey(key)).append(" = ");
valueWriter.write(fromValue, context);
context.output.append('\n');
}
}
// Now render (sub)tables and arrays of tables
for (Object key : from.keySet()) {
Object fromValue = from.get(key);
if (fromValue == null) {
continue;
}
ValueWriter valueWriter = WRITERS.findWriterFor(fromValue);
if (valueWriter.isTable() || valueWriter == TABLE_ARRAY_VALUE_WRITER) {
valueWriter.write(fromValue, context.pushTable(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, WriterContext context) {
for (Object key : values.keySet()) {
Object fromValue = values.get(key);
if (fromValue == null) {
continue;
}
ValueWriter valueWriter = WRITERS.findWriterFor(fromValue);
if (valueWriter.isPrimitiveType() || valueWriter == PRIMITIVE_ARRAY_VALUE_WRITER) {
return true;
}
}
return false;
}
private MapValueWriter() {}
}

View file

@ -2,7 +2,7 @@ package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
class NumberConverter implements ValueConverter {
class NumberConverter implements ValueConverter, ValueWriter {
static final NumberConverter NUMBER_PARSER = new NumberConverter();
@Override
@ -82,4 +82,29 @@ class NumberConverter implements ValueConverter {
return errors;
}
}
@Override
public boolean canWrite(Object value) {
return Number.class.isInstance(value);
}
@Override
public void write(Object value, WriterContext context) {
context.output.append(value.toString());
}
@Override
public boolean isPrimitiveType() {
return true;
}
@Override
public boolean isTable() {
return false;
}
@Override
public String toString() {
return "number";
}
}

View file

@ -0,0 +1,77 @@
package com.moandjiezana.toml;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import static com.moandjiezana.toml.MapValueWriter.MAP_VALUE_WRITER;
class ObjectValueWriter implements ValueWriter {
static final ValueWriter OBJECT_VALUE_WRITER = new ObjectValueWriter();
@Override
public boolean canWrite(Object value) {
return true;
}
@Override
public void write(Object value, WriterContext 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_VALUE_WRITER.write(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
Set<Field> prunedFields = new LinkedHashSet<Field>();
for (Field field : fields) {
if (!Modifier.isFinal(field.getModifiers())) {
prunedFields.add(field);
}
}
return prunedFields;
}
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;
}
private ObjectValueWriter() {}
}

View file

@ -0,0 +1,58 @@
package com.moandjiezana.toml;
import java.util.Collection;
import static com.moandjiezana.toml.ValueWriters.WRITERS;
class PrimitiveArrayValueWriter extends ArrayValueWriter {
static final ValueWriter PRIMITIVE_ARRAY_VALUE_WRITER = new PrimitiveArrayValueWriter();
@Override
public boolean canWrite(Object value) {
return isArrayish(value) && isArrayOfPrimitive(value);
}
@Override
public void write(Object value, WriterContext context) {
Collection values = normalize(value);
context.output.append('[');
if (!context.getTomlWriter().wantTerseArrays()) {
context.output.append(' ');
}
boolean first = true;
ValueWriter firstWriter = null;
for (Object elem : values) {
if (first) {
firstWriter = WRITERS.findWriterFor(elem);
first = false;
} else {
ValueWriter writer = WRITERS.findWriterFor(elem);
if (writer != firstWriter) {
throw new IllegalStateException(
context.getContextPath() +
": cannot write a heterogeneous array; first element was of type " + firstWriter +
" but found " + writer
);
}
context.output.append(", ");
}
WRITERS.write(elem, context);
}
if (!context.getTomlWriter().wantTerseArrays()) {
context.output.append(' ');
}
context.output.append(']');
}
private PrimitiveArrayValueWriter() {}
@Override
public String toString() {
return "primitive-array";
}
}

View file

@ -4,11 +4,23 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class StringConverter implements ValueConverter {
class StringConverter implements ValueConverter, ValueWriter {
static final StringConverter STRING_PARSER = new StringConverter();
private static final Pattern UNICODE_REGEX = Pattern.compile("\\\\[uU](.{4})");
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 canConvert(String s) {
return s.startsWith("\"");
@ -76,6 +88,50 @@ class StringConverter implements ValueConverter {
.replace("\\b", "\b")
.replace("\\f", "\f");
}
@Override
public boolean canWrite(Object value) {
return value.getClass().isAssignableFrom(String.class);
}
@Override
public void write(Object value, WriterContext context) {
context.output.append('"');
escapeUnicode(value.toString(), context.output);
context.output.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++;
}
}
}
private StringConverter() {}
@Override
public String toString() {
return "string";
}
}

View file

@ -0,0 +1,32 @@
package com.moandjiezana.toml;
import java.util.Collection;
import static com.moandjiezana.toml.ValueWriters.WRITERS;
class TableArrayValueWriter extends ArrayValueWriter {
static final ValueWriter TABLE_ARRAY_VALUE_WRITER = new TableArrayValueWriter();
@Override
public boolean canWrite(Object value) {
return isArrayish(value) && !isArrayOfPrimitive(value);
}
@Override
public void write(Object value, WriterContext context) {
Collection values = normalize(value);
WriterContext subContext = context.pushTableFromArray();
for (Object elem : values) {
WRITERS.write(elem, subContext);
}
}
private TableArrayValueWriter() {}
@Override
public String toString() {
return "table-array";
}
}

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.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
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>
*

View file

@ -0,0 +1,177 @@
package com.moandjiezana.toml;
import java.io.*;
import java.text.DateFormat;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import static com.moandjiezana.toml.ValueWriters.WRITERS;
/**
* <p>Converts Objects to TOML</p>
*
* <p>An input Object 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 output to TOML tables, and {@link List}s and Array to TOML arrays.</p>
*
* <p>Example usage:</p>
* <pre><code>
* class AClass {
* int anInt = 1;
* int[] anArray = { 2, 3 };
* }
*
* String tomlString = new TomlWriter().write(new AClass());
* </code></pre>
*/
public class TomlWriter {
private WriterIndentationPolicy indentationPolicy = new WriterIndentationPolicy();
private boolean wantTerseArraysValue = false;
private GregorianCalendar calendar = new GregorianCalendar();
private DateFormat customDateFormat = null;
/**
* Creates a TomlWriter instance.
*/
public TomlWriter() {}
/**
* Write an Object into TOML String.
*
* @param from the object to be written
* @return a string containing the TOML representation of the given Object
*/
public String write(Object from) {
return WRITERS.write(from, this);
}
/**
* Write an Object in TOML to a {@link Writer}.
*
* @param from the object to be written
* @param target the Writer to which TOML will be written
* @throws IOException if target.write() fails
*/
public void write(Object from, Writer target) throws IOException {
target.write(write(from));
}
/**
* Write an Object in TOML to a {@link OutputStream}.
*
* @param from the object to be written
* @param target the OutputStream to which the TOML will be written
* @throws IOException if target.write() fails
*/
public void write(Object from, OutputStream target) throws IOException {
target.write(write(from).getBytes());
}
/**
* Write an Object in TOML to a {@link File}.
*
* @param from the object to be written
* @param target the File to which the TOML will be written
* @throws IOException if any file operations fail
*/
public void write(Object from, File target) throws IOException {
FileWriter writer = new FileWriter(target);
writer.write(write(from));
writer.close();
}
public WriterIndentationPolicy getIndentationPolicy() {
return indentationPolicy;
}
/**
* Set the {@link WriterIndentationPolicy} for this writer.
*
* If unset, the default policy (no indentation) is used.
*
* @param indentationPolicy the new policy
* @return this TomlWriter instance
*/
public TomlWriter setIndentationPolicy(WriterIndentationPolicy indentationPolicy) {
this.indentationPolicy = indentationPolicy;
return this;
}
/**
* <p>Control whitespace in arrays in the TOML output.</p>
*
* <p>Terse arrays = false (default):</p>
*
* <pre><code>
* a = [ 1, 2, 3 ]
* </code></pre>
*
* <p>Terse arrays = true:</p>
*
* <pre><code>
* a = [1,2,3]
* </code></pre>
*
* @param value terse arrays setting
* @return this TomlWriter instance
*/
public TomlWriter wantTerseArrays(boolean value) {
this.wantTerseArraysValue = value;
return this;
}
/**
* Get the current array whitespace policy
* @return the current policy
*/
public boolean wantTerseArrays() {
return wantTerseArraysValue;
}
/**
* Set the {@link TimeZone} used when formatting datetimes.
*
* If unset, datetimes will be rendered in the current time zone.
*
* @param timeZone custom TimeZone.
* @return this TomlWriter instance
*/
public TomlWriter setTimeZone(TimeZone timeZone) {
calendar = new GregorianCalendar(timeZone);
return this;
}
/**
* Get the {@link TimeZone} in use for this TomlWriter.
*
* @return the currently set TimeZone.
*/
public TimeZone getTimeZone() {
return calendar.getTimeZone();
}
/**
* Override the default date format.
*
* If a time zone was set with {@link #setTimeZone(TimeZone)}, it will be applied before formatting
* datetimes.
*
* @param customDateFormat a custom DateFormat
* @return this TomlWriter instance
*/
public TomlWriter setDateFormat(DateFormat customDateFormat) {
this.customDateFormat = customDateFormat;
return this;
}
public DateFormat getDateFormat() {
return customDateFormat;
}
GregorianCalendar getCalendar() {
return calendar;
}
}

View file

@ -0,0 +1,11 @@
package com.moandjiezana.toml;
interface ValueWriter {
boolean canWrite(Object value);
void write(Object value, WriterContext context);
boolean isPrimitiveType();
boolean isTable();
}

View file

@ -0,0 +1,43 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.BooleanConverter.BOOLEAN_PARSER;
import static com.moandjiezana.toml.DateConverter.DATE_PARSER;
import static com.moandjiezana.toml.MapValueWriter.MAP_VALUE_WRITER;
import static com.moandjiezana.toml.NumberConverter.NUMBER_PARSER;
import static com.moandjiezana.toml.ObjectValueWriter.OBJECT_VALUE_WRITER;
import static com.moandjiezana.toml.PrimitiveArrayValueWriter.PRIMITIVE_ARRAY_VALUE_WRITER;
import static com.moandjiezana.toml.StringConverter.STRING_PARSER;
import static com.moandjiezana.toml.TableArrayValueWriter.TABLE_ARRAY_VALUE_WRITER;
class ValueWriters {
static final ValueWriters WRITERS = new ValueWriters();
ValueWriter findWriterFor(Object value) {
for (ValueWriter valueWriter : VALUE_WRITERs) {
if (valueWriter.canWrite(value)) {
return valueWriter;
}
}
return OBJECT_VALUE_WRITER;
}
String write(Object value, TomlWriter tomlWriter) {
WriterContext context = new WriterContext(tomlWriter);
write(value, context);
return context.output.toString();
}
void write(Object value, WriterContext context) {
findWriterFor(value).write(value, context);
}
private ValueWriters() {}
private static final ValueWriter[] VALUE_WRITERs = {
STRING_PARSER, NUMBER_PARSER, BOOLEAN_PARSER, DATE_PARSER,
MAP_VALUE_WRITER, PRIMITIVE_ARRAY_VALUE_WRITER, TABLE_ARRAY_VALUE_WRITER
};
}

View file

@ -0,0 +1,96 @@
package com.moandjiezana.toml;
import java.util.Arrays;
class WriterContext {
private String key = "";
private String currentTableIndent = "";
private String currentFieldIndent = "";
private String arrayKey = null;
private boolean isArrayOfTable = false;
private final TomlWriter tomlWriter;
StringBuilder output = new StringBuilder();
WriterContext(String key, String tableIndent, StringBuilder output, TomlWriter tomlWriter) {
this.key = key;
this.currentTableIndent = tableIndent;
this.currentFieldIndent = tableIndent + fillStringWithSpaces(tomlWriter.getIndentationPolicy().getKeyValueIndent());
this.output = output;
this.tomlWriter = tomlWriter;
}
WriterContext(TomlWriter tomlWriter) {
this.tomlWriter = tomlWriter;
}
WriterContext pushTable(String newKey) {
String newIndent = "";
if (!key.isEmpty()) {
newIndent = growIndent(tomlWriter.getIndentationPolicy());
}
String fullKey = key + (key.isEmpty() ? newKey : "." + newKey);
return new WriterContext(fullKey, newIndent, output, tomlWriter);
}
WriterContext pushTableFromArray() {
WriterContext subContext = new WriterContext(key, currentTableIndent, output, tomlWriter);
subContext.setIsArrayOfTable(true);
return subContext;
}
void writeKey() {
if (key.isEmpty()) {
return;
}
if (output.length() > 0) {
output.append('\n');
}
output.append(currentTableIndent);
if (isArrayOfTable) {
output.append("[[").append(key).append("]]\n");
} else {
output.append('[').append(key).append("]\n");
}
}
void indent() {
if (!key.isEmpty()) {
output.append(currentFieldIndent);
}
}
WriterContext setIsArrayOfTable(boolean isArrayOfTable) {
this.isArrayOfTable = isArrayOfTable;
return this;
}
private String growIndent(WriterIndentationPolicy indentationPolicy) {
return currentTableIndent + fillStringWithSpaces(indentationPolicy.getTableIndent());
}
private String fillStringWithSpaces(int count) {
char[] chars = new char[count];
Arrays.fill(chars, ' ');
return new String(chars);
}
public TomlWriter getTomlWriter() {
return tomlWriter;
}
public WriterContext setArrayKey(String arrayKey) {
this.arrayKey = arrayKey;
return this;
}
public String getContextPath() {
return key.isEmpty() ? arrayKey : key + "." + arrayKey;
}
}

View file

@ -0,0 +1,41 @@
package com.moandjiezana.toml;
/**
* Controls how a {@link TomlWriter} indents tables and key/value pairs.
*
* The default policy is to not indent.
*/
public class WriterIndentationPolicy {
private int tableIndent = 0;
private int keyValueIndent = 0;
public int getTableIndent() {
return tableIndent;
}
/**
* Sets the number of spaces a nested table name is indented.
*
* @param tableIndent number of spaces to indent
* @return this WriterIndentationPolicy instance
*/
public WriterIndentationPolicy setTableIndent(int tableIndent) {
this.tableIndent = tableIndent;
return this;
}
public int getKeyValueIndent() {
return keyValueIndent;
}
/**
* Sets the number of spaces key/value pairs within a table are indented.
*
* @param keyValueIndent number of spaces to indent
* @return this WriterIndentationPolicy instance
*/
public WriterIndentationPolicy setKeyValueIndent(int keyValueIndent) {
this.keyValueIndent = keyValueIndent;
return this;
}
}

View file

@ -1,6 +1,8 @@
package com.moandjiezana.toml;
import static org.junit.Assert.assertEquals;
import com.google.gson.*;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
@ -8,24 +10,11 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.*;
import org.junit.Ignore;
import org.junit.Test;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import static org.junit.Assert.assertEquals;
public class BurntSushiValidTest {
@ -43,11 +32,13 @@ public class BurntSushiValidTest {
@Test
public void arrays_hetergeneous() throws Exception {
run("arrays-hetergeneous");
runEncoder("arrays-hetergeneous");
}
@Test
public void arrays_nested() throws Exception {
run("arrays-nested");
runEncoder("arrays-nested");
}
@Test
@ -68,21 +59,25 @@ public class BurntSushiValidTest {
@Test
public void datetime() throws Exception {
run("datetime");
runEncoder("datetime");
}
@Test
public void empty() throws Exception {
run("empty");
runEncoder("empty");
}
@Test
public void example() throws Exception {
run("example");
runEncoder("example");
}
@Test
public void float_() throws Exception {
run("float");
runEncoder("float");
}
@Test
@ -93,16 +88,19 @@ public class BurntSushiValidTest {
@Test
public void implicit_and_explicit_before() throws Exception {
run("implicit-and-explicit-before");
runEncoder("implicit-and-explicit-before");
}
@Test
public void implicit_groups() throws Exception {
run("implicit-groups");
runEncoder("implicit-groups");
}
@Test
public void integer() throws Exception {
run("integer");
runEncoder("integer");
}
@Test
@ -128,6 +126,7 @@ public class BurntSushiValidTest {
@Test
public void key_special_chars_modified() throws Exception {
run("key-special-chars-modified");
runEncoder("key-special-chars-modified");
}
@Test @Ignore
@ -143,11 +142,13 @@ public class BurntSushiValidTest {
@Test
public void long_float() throws Exception {
run("long-float");
runEncoder("long-float");
}
@Test
public void long_integer() throws Exception {
run("long-integer");
runEncoder("long-integer");
}
@Test @Ignore
@ -173,6 +174,7 @@ public class BurntSushiValidTest {
@Test
public void string_empty() throws Exception {
run("string-empty");
runEncoder("string-empty");
}
@Test @Ignore
@ -183,11 +185,13 @@ public class BurntSushiValidTest {
@Test
public void string_escapes_modified() throws Exception {
run("string-escapes-modified");
runEncoder("string-escapes-modified");
}
@Test
public void string_simple() throws Exception {
run("string-simple");
runEncoder("string-simple");
}
@Test
@ -198,11 +202,13 @@ public class BurntSushiValidTest {
@Test
public void table_array_implicit() throws Exception {
run("table-array-implicit");
runEncoder("table-array-implicit");
}
@Test
public void table_array_many() throws Exception {
run("table-array-many");
runEncoder("table-array-many");
}
@Test
@ -210,9 +216,17 @@ public class BurntSushiValidTest {
run("table-array-nest");
}
@Test
public void table_array_nest_modified() throws Exception {
// Same test, but with stray spaces in the expected TOML removed
runEncoder("table-array-nest-modified",
new TomlWriter().setIndentationPolicy(new WriterIndentationPolicy().setTableIndent(2)));
}
@Test
public void table_array_one() throws Exception {
run("table-array-one");
runEncoder("table-array-one");
}
@Test
@ -246,7 +260,9 @@ public class BurntSushiValidTest {
}
private void run(String testName) {
InputStream inputToml = getClass().getResourceAsStream("burntsushi/valid/" + testName + ".toml");
InputStream inputTomlStream = getClass().getResourceAsStream("burntsushi/valid/" + testName + ".toml");
// InputStream inputToml = getClass().getResourceAsStream("burntsushi/valid/" + testName + ".toml");
String inputToml = convertStreamToString(inputTomlStream);
Reader expectedJsonReader = new InputStreamReader(getClass().getResourceAsStream("burntsushi/valid/" + testName + ".json"));
JsonElement expectedJson = GSON.fromJson(expectedJsonReader, JsonElement.class);
@ -254,16 +270,103 @@ public class BurntSushiValidTest {
JsonElement actual = TEST_GSON.toJsonTree(toml).getAsJsonObject().get("values");
assertEquals(expectedJson, actual);
try {
inputToml.close();
inputTomlStream.close();
} catch (IOException e) {}
try {
expectedJsonReader.close();
} catch (IOException e) {}
}
static String convertStreamToString(java.io.InputStream is) {
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
private void runEncoder(String testName) {
runEncoder(testName,
new TomlWriter().
wantTerseArrays(true).
setTimeZone(TimeZone.getTimeZone("UTC")).
setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")));
}
private void runEncoder(String testName, TomlWriter tomlWriter) {
InputStream inputTomlStream = getClass().getResourceAsStream("burntsushi/valid/" + testName + ".toml");
String expectedToml = convertStreamToString(inputTomlStream);
Reader inputJsonReader = new InputStreamReader(getClass().getResourceAsStream("burntsushi/valid/" + testName + ".json"));
JsonElement jsonInput = GSON.fromJson(inputJsonReader, JsonElement.class);
Map<String, Object> enriched = enrichJson(jsonInput.getAsJsonObject());
String encoded = tomlWriter.write(enriched);
assertEquals(expectedToml, encoded);
}
// Enrich toml-test JSON trees into native Java types, suiteable
// for consumption by TomlWriter.
private Map<String, Object> enrichJson(JsonObject jsonObject) {
Map<String, Object> enriched = new LinkedHashMap<String, Object>();
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
enriched.put(entry.getKey(), enrichJsonElement(entry.getValue()));
}
return enriched;
}
Object enrichJsonElement(JsonElement jsonElement) {
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject.has("type") && jsonObject.has("value")) {
return enrichPrimitive(jsonObject);
}
return enrichJson(jsonElement.getAsJsonObject());
} else if (jsonElement.isJsonArray()) {
List<Object> tables = new LinkedList<Object>();
for (JsonElement arrayElement : jsonElement.getAsJsonArray()) {
tables.add(enrichJsonElement(arrayElement));
}
return tables;
}
throw new AssertionError("received unexpected JsonElement: " + jsonElement);
}
private Object enrichPrimitive(JsonObject jsonObject) {
String type = jsonObject.getAsJsonPrimitive("type").getAsString();
if ("bool".equals(type)) {
return jsonObject.getAsJsonPrimitive("value").getAsBoolean();
} else if ("integer".equals(type)) {
return jsonObject.getAsJsonPrimitive("value").getAsBigInteger();
} else if ("float".equals(type)) {
return jsonObject.getAsJsonPrimitive("value").getAsDouble();
} else if ("string".equals(type)) {
return jsonObject.getAsJsonPrimitive("value").getAsString();
} else if ("datetime".equals(type)) {
DateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC"));
String dateString = jsonObject.getAsJsonPrimitive("value").getAsString();
try {
return iso8601Format.parse(dateString);
} catch (ParseException e) {
throw new AssertionError("failed to parse datetime '" + dateString + "': " + e.getMessage());
}
} else if ("array".equals(type)) {
JsonArray jsonArray = jsonObject.getAsJsonArray("value");
List<Object> enriched = new LinkedList<Object>();
for (JsonElement arrayElement : jsonArray) {
enriched.add(enrichJsonElement(arrayElement));
}
return enriched;
}
throw new AssertionError("enrichPrimitive: received unknown type " + type);
}
private static final Gson GSON = new Gson();
private static final Gson TEST_GSON = new GsonBuilder()
.registerTypeAdapter(Boolean.class, serialize(Boolean.class))

View file

@ -0,0 +1,319 @@
package com.moandjiezana.toml;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import static org.junit.Assert.assertEquals;
public class ValueWriterTest {
@Rule
public TemporaryFolder testDirectory = new TemporaryFolder();
@Test
public void should_write_primitive_types() {
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;
o.aDate = new Date();
String theDate = formatDate(o.aDate);
String output = new TomlWriter().write(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, output);
}
private String formatDate(Date date) {
// Copying the date formatting code from DateValueWriter isn't optimal, but
// I can't see any other way to check date formatting - the test gets
// run in multiple time zones, so we can't just hard-code a time zone.
String dateString = new SimpleDateFormat("yyyy-MM-dd'T'HH:m:ss").format(date);
Calendar calendar = new GregorianCalendar();
int tzOffset = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
dateString += String.format("%+03d:%02d", tzOffset / 60, tzOffset % 60);
return dateString;
}
class SubChild {
int anInt;
}
class Child {
SubChild subChild;
int anInt;
}
class Parent {
Map<String, Object> aMap;
Child child;
boolean aBoolean;
}
private Parent buildNestedMap() {
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;
return parent;
}
@Test
public void should_write_nested_map_with_default_indentation_policy() {
String output = new TomlWriter().write(buildNestedMap());
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, output);
}
@Test
public void should_follow_indentation_policy_of_indented_values() {
String output = new TomlWriter().
setIndentationPolicy(new WriterIndentationPolicy().setKeyValueIndent(2)).
write(buildNestedMap());
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, output);
}
@Test
public void should_follow_indentation_policy_of_indented_tables() {
String output = new TomlWriter().
setIndentationPolicy(new WriterIndentationPolicy().setTableIndent(2)).
write(buildNestedMap());
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, output);
}
@Test
public void should_follow_indentation_policy_of_indented_tables_and_values() {
String output = new TomlWriter().
setIndentationPolicy(new WriterIndentationPolicy().setTableIndent(2).setKeyValueIndent(2)).
write(buildNestedMap());
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, output);
}
@Test
public void should_write_array_of_tables() {
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 output = new TomlWriter().write(config);
String expected = "[[table]]\n" +
"anInt = 1\n\n" +
"[[table]]\n" +
"anInt = 2\n";
assertEquals(expected, output);
}
@Test
public void should_write_array_of_array() {
class ArrayTest {
int[][] array = {{1, 2, 3}, {4, 5, 6}};
}
ArrayTest arrayTest = new ArrayTest();
String output = new TomlWriter().write(arrayTest);
String expected = "array = [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]\n";
assertEquals(expected, output);
}
@Test
public void should_write_list() {
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", new TomlWriter().write(o));
}
@Test
public void should_handle_zero_length_arrays_and_lists() {
class TestClass {
List<Integer> aList = new LinkedList<Integer>();
Float[] anArray = new Float[0];
}
assertEquals("", new TomlWriter().write(new TestClass()));
}
@Test(expected = IllegalStateException.class)
public void should_reject_heterogeneous_arrays() {
class BadArray {
Object[] array = new Object[2];
}
BadArray badArray = new BadArray();
badArray.array[0] = new Integer(1);
badArray.array[1] = "oops";
new TomlWriter().write(badArray);
}
@Test
public void should_elide_empty_intermediate_tables() {
class C {
int anInt = 1;
}
class B {
C c = new C();
}
class A {
B b = new B();
}
assertEquals("[b.c]\nanInt = 1\n", new TomlWriter().write(new A()));
}
class Base {
protected int anInt = 2;
}
class Impl extends Base {
boolean aBoolean = true;
}
@Test
public void should_write_classes_with_inheritance() {
Impl impl = new Impl();
String expected = "aBoolean = true\nanInt = 2\n";
assertEquals(expected, new TomlWriter().write(impl));
}
@Test
public void should_write_strings_to_toml_utf8() throws UnsupportedEncodingException {
String input = " é foo € \b \t \n \f \r \" \\ ";
assertEquals("\" \\u00E9 foo \\u20AC \\b \\t \\n \\f \\r \\\" \\\\ \"", new TomlWriter().write(input));
// Check unicode code points greater than 0XFFFF
input = " \uD801\uDC28 \uD840\uDC0B ";
assertEquals("\" \\U00010428 \\U0002000B \"", new TomlWriter().write(input));
}
@Test
public void should_quote_keys() {
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, new TomlWriter().write(aMap));
}
private static class SimpleTestClass {
int a = 1;
}
@Test
public void should_write_to_writer() throws IOException {
StringWriter output = new StringWriter();
new TomlWriter().write(new SimpleTestClass(), output);
assertEquals("a = 1\n", output.toString());
}
@Test
public void should_write_to_outputstream() throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
new TomlWriter().write(new SimpleTestClass(), output);
assertEquals("a = 1\n", output.toString());
}
@Test
public void should_write_to_file() throws IOException {
File output = testDirectory.newFile();
new TomlWriter().write(new SimpleTestClass(), output);
assertEquals("a = 1\n", readFile(output));
}
private String readFile(File input) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new FileReader(input));
StringBuilder w = new StringBuilder();
String line = bufferedReader.readLine();
while (line != null) {
w.append(line).append('\n');
line = bufferedReader.readLine();
}
return w.toString();
}
}

View file

@ -0,0 +1,18 @@
{
"albums": [
{
"name": {"type": "string", "value": "Born to Run"},
"songs": [
{"name": {"type": "string", "value": "Jungleland"}},
{"name": {"type": "string", "value": "Meeting Across the River"}}
]
},
{
"name": {"type": "string", "value": "Born in the USA"},
"songs": [
{"name": {"type": "string", "value": "Glory Days"}},
{"name": {"type": "string", "value": "Dancing in the Dark"}}
]
}
]
}

View file

@ -0,0 +1,17 @@
[[albums]]
name = "Born to Run"
[[albums.songs]]
name = "Jungleland"
[[albums.songs]]
name = "Meeting Across the River"
[[albums]]
name = "Born in the USA"
[[albums.songs]]
name = "Glory Days"
[[albums.songs]]
name = "Dancing in the Dark"