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] [address]
street = "123 A Street" street = "123 A Street"
city = "AnyVille" city = "AnyVille"
[contacts]
"email address" = me@example.com
``` ```
```java ```java
@ -62,6 +65,7 @@ class Address {
class User { class User {
String name; String name;
Address address; 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.name.equals("Mwanji Ezana");
assert user.address.street.equals("123 A Street"); 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. 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 primitives can be mapped to a number of Java types:
TOML | Java 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. 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 ```toml
title = "TOML Example" title = "TOML Example"
"sub title" = "Now with quoted keys"
[database] [database]
ports = [ 8001, 8001, 8002 ] ports = [ 8001, 8001, 8002 ]
@ -139,6 +149,7 @@ title = "TOML Example"
Toml toml = new Toml().parse(getTomlFile()); Toml toml = new Toml().parse(getTomlFile());
String title = toml.getString("title"); String title = toml.getString("title");
String subTitle = toml.getString("\"sub title\"");
Boolean enabled = toml.getBoolean("database.enabled"); Boolean enabled = toml.getBoolean("database.enabled");
List<Long> ports = toml.getList("database.ports", Long.class); List<Long> ports = toml.getList("database.ports", Long.class);
String password = toml.getString("database.credentials.password"); String password = toml.getString("database.credentials.password");

View file

@ -7,7 +7,7 @@
</parent> </parent>
<groupId>com.moandjiezana.toml</groupId> <groupId>com.moandjiezana.toml</groupId>
<artifactId>toml4j</artifactId> <artifactId>toml4j</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>0.3.2-SNAPSHOT</version>
<name>toml4j</name> <name>toml4j</name>
<description>A parser for TOML</description> <description>A parser for TOML</description>
<url>http://moandjiezana.com/toml/toml4j</url> <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(); stack.pop();
} }
String[] tableParts = tableName.split("\\."); Keys.Key[] tableParts = Keys.split(tableName);
for (int i = 0; i < tableParts.length; i++) { for (int i = 0; i < tableParts.length; i++) {
String tablePart = tableParts[i]; String tablePart = tableParts[i].name;
Container currentContainer = stack.peek(); Container currentContainer = stack.peek();
if (tablePart.isEmpty()) { if (tablePart.isEmpty()) {
errors.append("Empty implicit table: " + tableName + "!\n"); errors.append("Empty implicit table: " + tableName + "!\n");

View file

@ -35,25 +35,16 @@ class StringConverter implements ValueConverter {
value = value.substring(1, stringTerminator); value = value.substring(1, stringTerminator);
value = replaceUnicodeCharacters(value); value = replaceUnicodeCharacters(value);
value = replaceSpecialCharacters(value);
chars = value.toCharArray(); if (value == null) {
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; return INVALID;
} }
}
value = replaceSpecialCharacters(value);
return value; return value;
} }
private String replaceUnicodeCharacters(String value) { String replaceUnicodeCharacters(String value) {
Matcher unicodeMatcher = UNICODE_REGEX.matcher(value); Matcher unicodeMatcher = UNICODE_REGEX.matcher(value);
while (unicodeMatcher.find()) { while (unicodeMatcher.find()) {
@ -62,7 +53,19 @@ class StringConverter implements ValueConverter {
return value; 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") return value.replace("\\n", "\n")
.replace("\\\"", "\"") .replace("\\\"", "\"")
.replace("\\t", "\t") .replace("\\t", "\t")

View file

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

View file

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