Initial commit

This commit is contained in:
moandji.ezana 2013-02-26 09:57:26 +02:00
commit 4ad8a683cc
9 changed files with 724 additions and 0 deletions

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# toml4j
toml4j is a [TOML](https://github.com/mojombo/toml) parser that uses the [Parboiled](http://www.parboiled.org) PEG parser.
## Usage
````java
Toml toml = new Toml(getTomlFile()); // throws an Exception if the TOML is incorrect
String title = toml.getString("title"); // if a key doesn't exist, returns null
Boolean enabled = toml.getBoolean("database.enabled"); // gets the key enabled from the key group database
Toml servers = toml.getKeyGroup("servers"); // returns a new Toml instance containing only the key group's values
````
### Defaults
The constructor can be given a set of default values that will be used if necessary.
````java
Toml toml = new Toml("a = 1", new Toml("a = 2\nb = 3");
Long a = toml.getLong("a"); // returns 1, not 2
Long b = toml.getLong("b"); // returns 3
Long c = toml.getLong("c"); // returns null
````

45
pom.xml Normal file
View file

@ -0,0 +1,45 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.sonatype.oss</groupId>
<artifactId>oss-parent</artifactId>
<version>7</version>
</parent>
<groupId>com.moandjiezana.toml</groupId>
<artifactId>toml4j</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>toml4j</name>
<description>A parser for TOML</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-java</artifactId>
<version>1.1.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.0</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,110 @@
package com.moandjiezana.toml;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.parboiled.Parboiled;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParsingResult;
/**
*
* All getters can fall back to default values if they have been provided and will return null if no matching key exists.
*
*/
public class Toml {
private final Map<String, Object> values;
private final Toml defaults;
public Toml(File file) throws FileNotFoundException {
this(new Scanner(file).useDelimiter("\\Z").next());
}
public Toml(String tomlString) {
this(tomlString, null);
}
public Toml(String tomlString, Toml defaults) {
TomlParser parser = Parboiled.createParser(TomlParser.class);
ParsingResult<Object> result = new RecoveringParseRunner<Object>(parser.Toml()).run(tomlString);
// ParsingResult<Object> parsingResult = new ReportingParseRunner<Object>(parser.Toml()).run(tomlString);
// System.out.println(ParseTreeUtils.printNodeTree(parsingResult));
TomlParser.Results results = (TomlParser.Results) result.valueStack.peek(result.valueStack.size() - 1);
if (results.errors.length() > 0) {
throw new IllegalStateException(results.errors.toString());
}
this.values = results.values;
this.defaults = defaults;
}
public String getString(String key) {
return (String) get(key);
}
public Long getLong(String key) {
return (Long) get(key);
}
@SuppressWarnings("unchecked")
public <T> List<T> getList(String key, Class<T> itemClass) {
return (List<T>) get(key);
}
public Boolean getBoolean(String key) {
return (Boolean) get(key);
}
public Date getDate(String key) {
return (Date) get(key);
}
public Double getDouble(String key) {
return (Double) get(key);
}
@SuppressWarnings("unchecked")
public Toml getKeyGroup(String key) {
return new Toml((Map<String, Object>) get(key));
}
@SuppressWarnings("unchecked")
private Object get(String key) {
String[] split = key.split("\\.");
Object current = new HashMap<String, Object>(values);
Object currentDefaults = defaults != null ? defaults.values : null;
for (String splitKey : split) {
current = ((Map<String, Object>) current).get(splitKey);
if (currentDefaults != null) {
currentDefaults = ((Map<String, Object>) currentDefaults).get(splitKey);
if (current instanceof Map && currentDefaults instanceof Map) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) currentDefaults).entrySet()) {
if (!((Map<String, Object>) current).containsKey(entry.getKey())) {
((Map<String, Object>) current).put(entry.getKey(), entry.getValue());
}
}
}
}
if (current == null && currentDefaults != null) {
current = currentDefaults;
}
if (current == null) {
return null;
}
}
return current;
}
private Toml(Map<String, Object> values) {
this.values = values;
this.defaults = null;
}
}

View file

@ -0,0 +1,209 @@
package com.moandjiezana.toml;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.parboiled.BaseParser;
import org.parboiled.Rule;
import org.parboiled.annotations.BuildParseTree;
import org.parboiled.annotations.SuppressNode;
@BuildParseTree
public class TomlParser extends BaseParser<Object> {
static class Results {
public Map<String, Object> values = new HashMap<String, Object>();
public StringBuilder errors = new StringBuilder();
}
public Rule Toml() {
return Sequence(push(new TomlParser.Results()), push(((TomlParser.Results) peek()).values), OneOrMore(FirstOf(KeyGroup(), Comment(), Key())));
}
Rule KeyGroup() {
return Sequence(KeyGroupDelimiter(), KeyGroupName(), addKeyGroup((String) pop()), KeyGroupDelimiter(), Spacing());
}
Rule Key() {
return Sequence(Spacing(), KeyName(), EqualsSign(), VariableValues(), Spacing(), swap(), addKey((String) pop(), pop()));
}
Rule KeyGroupName() {
return Sequence(OneOrMore(FirstOf(Letter(), Digit(), '.', '_')), push(match()));
}
Rule KeyName() {
return Sequence(OneOrMore(FirstOf(Letter(), Digit(), '_', '.')), push(match()));
}
Rule VariableValues() {
return FirstOf(ArrayValue(), DateValue(), BooleanValue(), NumberValue(), StringValue());
}
Rule ArrayValue() {
return Sequence(push(ArrayList.class), '[', Spacing(), ZeroOrMore(VariableValues(), Optional(ArrayDelimiter())), Spacing(), ']', pushList());
}
Rule DateValue() {
return Sequence(Sequence(Year(), '-', Month(), '-', Day(), 'T', Digit(), Digit(), ':', Digit(), Digit(), ':', Digit(), Digit(), 'Z'), pushDate(match()));
}
Rule BooleanValue() {
return Sequence(FirstOf("true", "false"), push(Boolean.valueOf(match())));
}
Rule NumberValue() {
return Sequence(OneOrMore(FirstOf(Digit(), '.')), pushNumber(match()));
}
Rule StringValue() {
return Sequence('"', OneOrMore(TestNot('"'), ANY), pushString(match()), '"');
}
Rule Year() {
return Sequence(Digit(), Digit(), Digit(), Digit());
}
Rule Month() {
return Sequence(CharRange('0', '1'), Digit());
}
Rule Day() {
return Sequence(CharRange('0', '3'), Digit());
}
Rule Digit() {
return CharRange('0', '9');
}
Rule Letter() {
return CharRange('a', 'z');
}
@SuppressNode
Rule KeyGroupDelimiter() {
return AnyOf("[]");
}
@SuppressNode
Rule EqualsSign() {
return Sequence(Spacing(), '=', Spacing());
}
@SuppressNode
Rule Spacing() {
return ZeroOrMore(FirstOf(Comment(), AnyOf(" \t\r\n\f")));
}
@SuppressNode
Rule ArrayDelimiter() {
return Sequence(Spacing(), ',', Spacing());
}
@SuppressNode
Rule Comment() {
return Sequence('#', ZeroOrMore(TestNot(AnyOf("\r\n")), ANY), FirstOf("\r\n", '\r', '\n', EOI));
}
@SuppressWarnings("unchecked")
boolean addKeyGroup(String name) {
String[] split = name.split("\\.");
name = split[split.length - 1];
while (getContext().getValueStack().size() > 2) {
drop();
}
Map<String, Object> newKeyGroup = (Map<String, Object>) getContext().getValueStack().peek();
for (String splitKey : split) {
if (!newKeyGroup.containsKey(splitKey)) {
newKeyGroup.put(splitKey, new HashMap<String, Object>());
}
Object currentValue = newKeyGroup.get(splitKey);
if (!(currentValue instanceof Map)) {
results().errors.append("Could not create key group ").append(name).append(": key already exists!");
return true;
}
newKeyGroup = (Map<String, Object>) currentValue;
}
push(newKeyGroup);
return true;
}
boolean addKey(String key, Object value) {
if (key.contains(".")) {
results().errors.append(key).append(" is invalid: key names may not contain a dot!\n");
return true;
}
putValue(key, value);
return true;
}
boolean pushList() {
ArrayList<Object> list = new ArrayList<Object>();
while (peek() != ArrayList.class) {
list.add(0, pop());
}
poke(list);
return true;
}
boolean pushDate(String dateString) {
String s = dateString.replace("Z", "+00:00");
try {
s = s.substring(0, 22) + s.substring(23);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
dateFormat.setLenient(false);
Date date = dateFormat.parse(s);
push(date);
return true;
} catch (Exception e) {
results().errors.append("Invalid date: ").append(dateString);
return false;
}
}
boolean pushNumber(String number) {
if (number.contains(".")) {
push(Double.valueOf(number));
} else {
push(Long.valueOf(number));
}
return true;
}
boolean pushString(String line) {
StringBuilder builder = new StringBuilder();
String[] split = line.split("\\\\n");
for (String string : split) {
builder.append(string).append('\n');
}
builder.deleteCharAt(builder.length() - 1);
push(builder.toString());
return true;
}
@SuppressWarnings("unchecked")
void putValue(String name, Object value) {
Map<String, Object> values = (Map<String, Object>) peek();
if (values.containsKey(name)) {
results().errors.append("Key ").append(name).append(" already exists!");
return;
}
values.put(name, value);
}
TomlParser.Results results() {
return (Results) peek(getContext().getValueStack().size() - 1);
}
}

View file

@ -0,0 +1,82 @@
package com.moandjiezana.toml;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Map;
import java.util.Scanner;
import java.util.TimeZone;
import org.junit.Test;
import org.parboiled.Parboiled;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParsingResult;
public class RealWorldTest {
@SuppressWarnings("unchecked")
@Test
public void should_parse_example() throws Exception {
TomlParser parser = Parboiled.createParser(TomlParser.class);
String toml = new Scanner(new File(getClass().getResource("/example.toml").getFile())).useDelimiter("\\Z").next();
ParsingResult<Object> result = new RecoveringParseRunner<Object>(parser.Toml()).run(toml);
Map<String, Object> root = (Map<String, Object>) result.valueStack.peek(result.valueStack.size() - 2);
printMap(root);
assertEquals("TOML Example", root.get("title"));
Map<String, Object> owner = get(root, "owner");
assertEquals("Tom Preston-Werner", owner.get("name"));
assertEquals("GitHub", owner.get("organization"));
assertEquals("GitHub Cofounder & CEO\nLikes tater tots and beer.", owner.get("bio"));
Calendar dob = Calendar.getInstance();
dob.set(1979, Calendar.MAY, 27, 7, 32, 0);
dob.set(Calendar.MILLISECOND, 0);
dob.setTimeZone(TimeZone.getTimeZone("UTC"));
assertEquals(dob.getTime(), owner.get("dob"));
Map<String, Object> database = get(root, "database");
assertEquals("192.168.1.1", database.get("server"));
assertEquals(5000L, database.get("connection_max"));
assertTrue((Boolean) database.get("enabled"));
assertEquals(Arrays.asList(8001L, 8001L, 8002L), database.get("ports"));
Map<String, Object> servers = get(root, "servers");
Map<String, Object> alphaServers = get(servers, "alpha");
assertEquals("10.0.0.1", alphaServers.get("ip"));
assertEquals("eqdc10", alphaServers.get("dc"));
Map<String, Object> betaServers = get(servers, "beta");
assertEquals("10.0.0.2", betaServers.get("ip"));
assertEquals("eqdc10", betaServers.get("dc"));
Map<String, Object> clients = get(root, "clients");
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.get("data"));
assertEquals(asList("alpha", "omega"), clients.get("hosts"));
}
@SuppressWarnings("unchecked")
private void printMap(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Map) {
System.out.println("[" + entry.getKey() + "]");
printMap((Map<String, Object>) entry.getValue());
System.out.println("[/" + entry.getKey() + "]");
} else {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
}
}
@SuppressWarnings("unchecked")
private Map<String, Object> get(Map<String, Object> map, String key) {
return (Map<String, Object>) map.get(key);
}
}

View file

@ -0,0 +1,61 @@
package com.moandjiezana.toml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import org.junit.Before;
import org.junit.Test;
public class TomlDefaultsTest {
private Toml defaultToml;
@Before
public void before() {
defaultToml = new Toml("a = \"a\"\n[group]\na=\"a\"");
}
@Test
public void should_fall_back_to_default_value() {
Toml toml = new Toml("", defaultToml);
assertEquals("a", toml.getString("a"));
}
@Test
public void should_use_value_when_present_in_values_and_defaults() {
Toml toml = new Toml("a = \"b\"", defaultToml);
assertEquals("b", toml.getString("a"));
}
@Test
public void should_return_null_when_no_defaults_for_key() throws Exception {
Toml toml = new Toml("", defaultToml);
assertNull(toml.getString("b"));
}
@Test
public void should_fall_back_to_default_with_multi_key() throws Exception {
Toml toml = new Toml("", defaultToml);
assertEquals("a", toml.getString("group.a"));
}
@Test
public void should_fall_back_to_key_group() throws Exception {
Toml toml = new Toml("", defaultToml);
assertEquals("a", toml.getKeyGroup("group").getString("a"));
}
@Test
public void should_fall_back_to_key_within_key_group() throws Exception {
Toml toml = new Toml("[group]\nb=1", defaultToml);
assertEquals(1, toml.getKeyGroup("group").getLong("b").intValue());
assertEquals("a", toml.getKeyGroup("group").getString("a"));
}
}

View file

@ -0,0 +1,156 @@
package com.moandjiezana.toml;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Calendar;
import java.util.TimeZone;
import org.junit.Test;
public class TomlTest {
@Test
public void should_get_string() throws Exception {
Toml toml = new Toml("a = \"a\"");
assertEquals("a", toml.getString("a"));
}
@Test
public void should_get_number() throws Exception {
Toml toml = new Toml("b = 1001");
assertEquals(1001, toml.getLong("b").intValue());
}
@Test
public void should_get_list() throws Exception {
Toml toml = new Toml("list = [\"a\", \"b\", \"c\"]");
assertEquals(asList("a", "b", "c"), toml.getList("list", String.class));
}
@Test
public void should_get_boolean() throws Exception {
Toml toml = new Toml("bool_false = false\nbool_true = true");
assertFalse(toml.getBoolean("bool_false"));
assertTrue(toml.getBoolean("bool_true"));
}
@Test
public void should_get_date() throws Exception {
Toml toml = new Toml("a_date = 2011-11-10T13:12:00Z");
Calendar calendar = Calendar.getInstance();
calendar.set(2011, Calendar.NOVEMBER, 10, 13, 12, 00);
calendar.set(Calendar.MILLISECOND, 0);
calendar.setTimeZone(TimeZone.getTimeZone("UTC"));
assertEquals(calendar.getTime(), toml.getDate("a_date"));
}
@Test
public void should_get_double() throws Exception {
Toml toml = new Toml("double = 5.25");
assertEquals(5.25D, toml.getDouble("double").doubleValue(), 0.0);
}
@Test
public void should_get_key_group() throws Exception {
Toml toml = new Toml("[group]\nkey = \"value\"");
Toml group = toml.getKeyGroup("group");
assertEquals("value", group.getString("key"));
}
@Test
public void should_get_value_for_multi_key() throws Exception {
Toml toml = new Toml("[group]\nkey = \"value\"");
assertEquals("value", toml.getString("group.key"));
}
@Test
public void should_get_value_for_multi_key_with_no_parent_keygroup() throws Exception {
Toml toml = new Toml("[group.sub]\nkey = \"value\"");
assertEquals("value", toml.getString("group.sub.key"));
}
@Test
public void should_return_null_if_no_value_for_key() throws Exception {
Toml toml = new Toml("");
assertNull(toml.getString("a"));
}
@Test
public void should_return_null_when_no_value_for_multi_key() throws Exception {
Toml toml = new Toml("");
assertNull(toml.getString("group.key"));
}
@Test
public void should_load_from_file() throws Exception {
Toml toml = new Toml(new File(getClass().getResource("should_load_from_file.toml").getFile()));
assertEquals("value", toml.getString("key"));
}
@Test
public void should_support_numbers_in_key_names() throws Exception {
Toml toml = new Toml("a1 = 1");
assertEquals(1, toml.getLong("a1").intValue());
}
@Test
public void should_support_numbers_in_key_group_names() throws Exception {
Toml toml = new Toml("[group1]\na = 1");
assertEquals(1, toml.getLong("group1.a").intValue());
}
@Test
public void should_support_underscores_in_key_names() throws Exception {
Toml toml = new Toml("a_a = 1");
assertEquals(1, toml.getLong("a_a").intValue());
}
@Test
public void should_support_underscores_in_key_group_names() throws Exception {
Toml toml = new Toml("[group_a]\na = 1");
assertEquals(1, toml.getLong("group_a.a").intValue());
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_dot_in_key_name() throws Exception {
new Toml("a.a = 1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_invalid_date() throws Exception {
new Toml("d = 2012-13-01T15:00:00Z");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_key_is_overwritten_by_key_group() {
new Toml("[fruit]\ntype=\"apple\"\n[fruit.type]\napple=\"yes\"");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_key_is_overwritten_by_another_key() {
new Toml("[fruit]\ntype=\"apple\"\ntype=\"orange\"");
}
}

View file

@ -0,0 +1 @@
key = "value"

View file

@ -0,0 +1,35 @@
# This is a TOML document. Boom.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# You can indent as you please. Tabs or spaces. TOML don't care.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]