Merge branch 'quoted_keys' into wip

Conflicts:
	README.md
This commit is contained in:
moandji.ezana 2015-01-22 12:20:06 +02:00
commit 89d2c70b15
8 changed files with 251 additions and 74 deletions

View file

@ -51,6 +51,9 @@ name = "Mwanji Ezana"
[address]
street = "123 A Street"
city = "AnyVille"
[contacts]
"email address" = me@example.com
```
```java
@ -62,6 +65,7 @@ class Address {
class User {
String name;
Address address;
Map<String, Object> contacts;
}
```
@ -70,10 +74,13 @@ User user = new Toml().parse(tomlFile).to(User.class);
assert user.name.equals("Mwanji Ezana");
assert user.address.street.equals("123 A Street");
assert user.contacts.get("\"email address\"").equals("me@example.com");
```
Any keys not found in both the TOML and the class are ignored. Fields may be private.
Quoted keys cannot be mapped directly to a Java object, but they can be used as keys within a `Map`.
TOML primitives can be mapped to a number of Java types:
TOML | Java
@ -105,8 +112,11 @@ You can also navigate values within a table with a compound key of the form `tab
Non-existent keys return null.
When retrieving quoted keys, the quotes must be used and the key must be spelled exactly the same way, including quotes and whitespace.
```toml
title = "TOML Example"
"sub title" = "Now with quoted keys"
[database]
ports = [ 8001, 8001, 8002 ]
@ -139,6 +149,7 @@ title = "TOML Example"
Toml toml = new Toml().parse(getTomlFile());
String title = toml.getString("title");
String subTitle = toml.getString("\"sub title\"");
Boolean enabled = toml.getBoolean("database.enabled");
List<Long> ports = toml.getList("database.ports", Long.class);
String password = toml.getString("database.credentials.password");

View file

@ -7,7 +7,7 @@
</parent>
<groupId>com.moandjiezana.toml</groupId>
<artifactId>toml4j</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>0.3.2-SNAPSHOT</version>
<name>toml4j</name>
<description>A parser for TOML</description>
<url>http://moandjiezana.com/toml/toml4j</url>

View file

@ -0,0 +1,102 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.isComment;
import java.util.ArrayList;
import java.util.List;
class Keys {
static class Key {
final String name;
final int index;
final String path;
Key(String name, int index, Key next) {
this.name = name;
this.index = index;
if (next != null) {
this.path = name + "." + next.path;
} else {
this.path = name;
}
}
}
static Keys.Key[] split(String key) {
List<Key> splitKey = new ArrayList<Key>();
StringBuilder current = new StringBuilder();
char[] chars = key.toCharArray();
boolean quoted = false;
boolean indexable = true;
boolean inIndex = false;
int index = -1;
for (int i = chars.length - 1; i > -1; i--) {
char c = chars[i];
if (c == ']' && indexable) {
inIndex = true;
continue;
}
indexable = false;
if (c == '[' && inIndex) {
inIndex = false;
index = Integer.parseInt(current.toString());
current = new StringBuilder();
continue;
}
if (c == '"' && (i == 0 || chars[i - 1] != '\\')) {
quoted = !quoted;
indexable = false;
}
if (c != '.' || quoted) {
current.insert(0, c);
} else {
splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null));
indexable = true;
index = -1;
current = new StringBuilder();
}
}
splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null));
return splitKey.toArray(new Key[0]);
}
/**
* @param line raw TOML iine to parse
* @return null if line is not a valid table identifier
*/
static String getTableName(String line) {
StringBuilder sb = new StringBuilder();
char[] chars = line.toCharArray();
boolean quoted = false;
boolean terminated = false;
for (int i = 1; i < chars.length; i++) {
char c = chars[i];
if (c == '"' && chars[i - 1] != '\\') {
quoted = !quoted;
} else if (!quoted && c == ']') {
terminated = true;
break;
} else if (!quoted && c == '[') {
break;
}
sb.append(c);
}
String tableName = sb.toString();
if (!terminated || !isComment(line.substring(tableName.length() + 2))) {
return null;
}
tableName = StringConverter.STRING_PARSER.replaceUnicodeCharacters(tableName);
return tableName;
}
private Keys() {}
}

View file

@ -75,9 +75,9 @@ class Results {
stack.pop();
}
String[] tableParts = tableName.split("\\.");
Keys.Key[] tableParts = Keys.split(tableName);
for (int i = 0; i < tableParts.length; i++) {
String tablePart = tableParts[i];
String tablePart = tableParts[i].name;
Container currentContainer = stack.peek();
if (tablePart.isEmpty()) {
errors.append("Empty implicit table: " + tableName + "!\n");

View file

@ -35,25 +35,16 @@ class StringConverter implements ValueConverter {
value = value.substring(1, stringTerminator);
value = replaceUnicodeCharacters(value);
chars = value.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
char ch = chars[i];
char next = chars[i + 1];
if (ch == '\\' && next == '\\') {
i++;
} else if (ch == '\\' && !(next == 'b' || next == 'f' || next == 'n' || next == 't' || next == 'r' || next == '"' || next == '/' || next == '\\')) {
return INVALID;
}
}
value = replaceSpecialCharacters(value);
if (value == null) {
return INVALID;
}
return value;
}
private String replaceUnicodeCharacters(String value) {
String replaceUnicodeCharacters(String value) {
Matcher unicodeMatcher = UNICODE_REGEX.matcher(value);
while (unicodeMatcher.find()) {
@ -62,7 +53,19 @@ class StringConverter implements ValueConverter {
return value;
}
private String replaceSpecialCharacters(String value) {
String replaceSpecialCharacters(String value) {
char[] chars = value.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
char ch = chars[i];
char next = chars[i + 1];
if (ch == '\\' && next == '\\') {
i++;
} else if (ch == '\\' && !(next == 'b' || next == 'f' || next == 'n' || next == 't' || next == 'r' || next == '"' || next == '/' || next == '\\')) {
return null;
}
}
return value.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\t", "\t")

View file

@ -12,14 +12,12 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.Gson;
@ -33,13 +31,13 @@ import com.google.gson.JsonElement;
* {@link #getList(String, Class)}, {@link #getTable(String)} and {@link #getTables(String)} return empty values if there is no matching key.</p>
*
* <p>Example usage:</p>
* <code><pre>
* <pre><code>
* Toml toml = new Toml().parse(getTomlFile());
* String name = toml.getString("name");
* Long port = toml.getLong("server.ip"); // compound key. Is equivalent to:
* Long port2 = toml.getTable("server").getLong("ip");
* MyConfig config = toml.to(MyConfig.class);
* </pre></code>
* </code></pre>
*
*/
public class Toml {
@ -67,7 +65,7 @@ public class Toml {
/**
* Populates the current Toml instance with values from file.
*
* @param file
* @param file The File to be read
* @return this instance
* @throws IllegalStateException If file contains invalid TOML
*/
@ -82,7 +80,7 @@ public class Toml {
/**
* Populates the current Toml instance with values from inputStream.
*
* @param inputStream
* @param inputStream Closed after it has been read.
* @return this instance
* @throws IllegalStateException If file contains invalid TOML
*/
@ -93,7 +91,7 @@ public class Toml {
/**
* Populates the current Toml instance with values from reader.
*
* @param reader
* @param reader Closed after it has been read.
* @return this instance
* @throws IllegalStateException If file contains invalid TOML
*/
@ -122,7 +120,7 @@ public class Toml {
/**
* Populates the current Toml instance with values from tomlString.
*
* @param tomlString
* @param tomlString String to be read.
* @return this instance
* @throws IllegalStateException If tomlString is not valid TOML
*/
@ -169,9 +167,8 @@ public class Toml {
}
/**
* If no value is found for key, an empty Toml instance is returned.
*
* @param key
* @param key A table name, not including square brackets.
* @return A new Toml instance. Empty if no value is found for key.
*/
@SuppressWarnings("unchecked")
public Toml getTable(String key) {
@ -179,8 +176,8 @@ public class Toml {
}
/**
* If no value is found for key, an empty list is returned.
* @param key
* @param key Name of array of tables, not including square brackets.
* @return An empty List if no value is found for key.
*/
@SuppressWarnings("unchecked")
public List<Toml> getTables(String key) {
@ -214,7 +211,9 @@ public class Toml {
* <li>TOML array to {@link Set}</li>
* </ul>
*
* @param targetClass
* @param targetClass Class to deserialize TOML to.
* @param <T> type of targetClass.
* @return A new instance of targetClass.
*/
public <T> T to(Class<T> targetClass) {
return to(targetClass, DEFAULT_GSON);
@ -245,55 +244,32 @@ public class Toml {
@SuppressWarnings("unchecked")
private Object get(String key) {
String[] split = key.split("\\.");
if (values.containsKey(key)) {
return values.get(key);
}
Object current = new HashMap<String, Object>(values);
for (int i = 0; i < split.length; i++) {
if (i == 0 && values.containsKey(key)) {
return values.get(key);
}
String keyWithDot = join(Arrays.copyOfRange(split, i, split.length));
if (current instanceof Map && ((Map<String, Object>) current).containsKey(keyWithDot)) {
return ((Map<String, Object>) current).get(keyWithDot);
}
String splitKey = split[i];
Matcher matcher = ARRAY_INDEX_PATTERN.matcher(splitKey);
int index = -1;
if (matcher.find()) {
splitKey = matcher.group(1);
index = Integer.parseInt(matcher.group(2), 10);
Keys.Key[] keys = Keys.split(key);
for (Keys.Key k : keys) {
if (k.index == -1 && current instanceof Map && ((Map<String, Object>) current).containsKey(k.path)) {
return ((Map<String, Object>) current).get(k.path);
}
current = ((Map<String, Object>) current).get(splitKey);
current = ((Map<String, Object>) current).get(k.name);
if (index > -1 && current != null) {
current = ((List<?>) current).get(index);
if (k.index > -1 && current != null) {
current = ((List<?>) current).get(k.index);
}
if (current == null) {
return defaults != null ? defaults.get(key) : null;
}
}
return current;
}
private String join(String[] strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string).append('.');
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
private Toml(Toml defaults, Map<String, Object> values) {
this.values = values != null ? values : Collections.<String, Object>emptyMap();

View file

@ -186,12 +186,7 @@ class TomlParser {
}
private String getTableName(String line) {
List<Object> resultValue = parse(parser().Table(), line);
if (resultValue == null) {
return null;
}
return (String) resultValue.get(0);
return Keys.getTableName(line);
}
private boolean isKeyValid(String key) {

View file

@ -0,0 +1,90 @@
package com.moandjiezana.toml;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.util.Map;
import org.junit.Test;
public class QuotedKeysTest {
@Test
public void should_accept_quoted_key_for_value() throws Exception {
Toml toml = new Toml().parse("\"127.0.0.1\" = \"localhost\" \n \"character encoding\" = \"UTF-8\" \n \"ʎǝʞ\" = \"value\"");
assertEquals("localhost", toml.getString("\"127.0.0.1\""));
assertEquals("UTF-8", toml.getString("\"character encoding\""));
assertEquals("value", toml.getString("\"ʎǝʞ\""));
}
@Test
public void should_accept_quoted_key_for_table_name() throws Exception {
Toml toml = new Toml().parse("[\"abc def\"]\n val = 1");
assertEquals(1L, toml.getTable("\"abc def\"").getLong("val").longValue());
}
@Test
public void should_accept_partially_quoted_table_name() throws Exception {
Toml toml = new Toml().parse("[dog.\"tater.man\"] \n type = \"pug0\" \n[dog.tater] \n type = \"pug1\"\n[dog.tater.man] \n type = \"pug2\"");
Toml dogs = toml.getTable("dog");
assertEquals("pug0", dogs.getTable("\"tater.man\"").getString("type"));
assertEquals("pug1", dogs.getTable("tater").getString("type"));
assertEquals("pug2", dogs.getTable("tater").getTable("man").getString("type"));
assertEquals("pug0", toml.getString("dog.\"tater.man\".type"));
assertEquals("pug2", toml.getString("dog.tater.man.type"));
}
@Test
@SuppressWarnings("unchecked")
public void should_conserve_quoted_key_in_map() throws Exception {
Toml toml = new Toml().parse("[dog.\"tater.man\"] \n type = \"pug0\" \n[dog.tater] \n type = \"pug1\"\n[dog.tater.man] \n type = \"pug2\"");
Toml dogs = toml.getTable("dog");
Map<String, Map<String, Object>> map = dogs.to(Map.class);
assertEquals("pug0", map.get("\"tater.man\"").get("type"));
assertEquals("pug1", map.get("tater").get("type"));
assertEquals("pug2", ((Map<String, Object>) map.get("tater").get("man")).get("type"));
}
@Test
public void should_convert_quoted_keys_to_map_but_not_to_object_fields() throws Exception {
Quoted quoted = new Toml().parse("\"ʎǝʞ\" = \"value\" \n[map] \n \"ʎǝʞ\" = \"value\"").to(Quoted.class);
assertNull(quoted.ʎǝʞ);
assertEquals("value", quoted.map.get("\"ʎǝʞ\""));
}
@Test
public void should_support_table_array_index_with_quoted_key() throws Exception {
Toml toml = new Toml().parse("[[dog.\" type\"]] \n name = \"type0\" \n [[dog.\" type\"]] \n name = \"type1\"");
assertEquals("type0", toml.getString("dog.\" type\"[0].name"));
assertEquals("type1", toml.getString("dog.\" type\"[1].name"));
}
@Test
public void should_support_quoted_key_containing_square_brackets() throws Exception {
Toml toml = new Toml().parse("[dog.\" type[abc]\"] \n name = \"type0\" \n [dog.\" type[1]\"] \n \"name[]\" = \"type1\"");
assertEquals("type0", toml.getString("dog.\" type[abc]\".name"));
assertEquals("type1", toml.getString("dog.\" type[1]\".\"name[]\""));
}
@Test
public void should_support_quoted_key_containing_escaped_quote() throws Exception {
Toml toml = new Toml().parse("[dog.\"ty\\\"pe\"] \n \"na\\\"me\" = \"type0\"");
assertEquals("type0", toml.getString("dog.\"ty\\\"pe\".\"na\\\"me\""));
}
private static class Quoted {
String ʎǝʞ;
Map<String, Object> map;
}
}