diff --git a/README.md b/README.md index 59afaac..f3a69a8 100644 --- a/README.md +++ b/README.md @@ -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 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 ports = toml.getList("database.ports", Long.class); String password = toml.getString("database.credentials.password"); diff --git a/pom.xml b/pom.xml index b399907..c3e60bf 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.moandjiezana.toml toml4j - 1.0.0-SNAPSHOT + 0.3.2-SNAPSHOT toml4j A parser for TOML http://moandjiezana.com/toml/toml4j diff --git a/src/main/java/com/moandjiezana/toml/Keys.java b/src/main/java/com/moandjiezana/toml/Keys.java new file mode 100644 index 0000000..bffb319 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/Keys.java @@ -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 splitKey = new ArrayList(); + 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() {} +} diff --git a/src/main/java/com/moandjiezana/toml/Results.java b/src/main/java/com/moandjiezana/toml/Results.java index 84e5a53..1a3ae9b 100644 --- a/src/main/java/com/moandjiezana/toml/Results.java +++ b/src/main/java/com/moandjiezana/toml/Results.java @@ -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"); diff --git a/src/main/java/com/moandjiezana/toml/StringConverter.java b/src/main/java/com/moandjiezana/toml/StringConverter.java index 05acfe7..2971e27 100644 --- a/src/main/java/com/moandjiezana/toml/StringConverter.java +++ b/src/main/java/com/moandjiezana/toml/StringConverter.java @@ -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") diff --git a/src/main/java/com/moandjiezana/toml/Toml.java b/src/main/java/com/moandjiezana/toml/Toml.java index 740e40f..f79b5b9 100644 --- a/src/main/java/com/moandjiezana/toml/Toml.java +++ b/src/main/java/com/moandjiezana/toml/Toml.java @@ -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.

* *

Example usage:

- *
+ * 

  * 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);
- * 
+ *
* */ 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 getTables(String key) { @@ -214,7 +211,9 @@ public class Toml { *
  • TOML array to {@link Set}
  • * * - * @param targetClass + * @param targetClass Class to deserialize TOML to. + * @param type of targetClass. + * @return A new instance of targetClass. */ public T to(Class 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(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) current).containsKey(keyWithDot)) { - return ((Map) 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) current).containsKey(k.path)) { + return ((Map) current).get(k.path); } - current = ((Map) current).get(splitKey); + current = ((Map) 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 values) { this.values = values != null ? values : Collections.emptyMap(); diff --git a/src/main/java/com/moandjiezana/toml/TomlParser.java b/src/main/java/com/moandjiezana/toml/TomlParser.java index d514c86..8e3594b 100644 --- a/src/main/java/com/moandjiezana/toml/TomlParser.java +++ b/src/main/java/com/moandjiezana/toml/TomlParser.java @@ -186,12 +186,7 @@ class TomlParser { } private String getTableName(String line) { - List resultValue = parse(parser().Table(), line); - if (resultValue == null) { - return null; - } - - return (String) resultValue.get(0); + return Keys.getTableName(line); } private boolean isKeyValid(String key) { diff --git a/src/test/java/com/moandjiezana/toml/QuotedKeysTest.java b/src/test/java/com/moandjiezana/toml/QuotedKeysTest.java new file mode 100644 index 0000000..0e8402d --- /dev/null +++ b/src/test/java/com/moandjiezana/toml/QuotedKeysTest.java @@ -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> map = dogs.to(Map.class); + + assertEquals("pug0", map.get("\"tater.man\"").get("type")); + assertEquals("pug1", map.get("tater").get("type")); + assertEquals("pug2", ((Map) 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 map; + } +}