package dev.plex.toml; import com.google.gson.Gson; import com.google.gson.JsonElement; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.StandardCharsets; 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 lombok.Getter; /** *

Provides access to the keys and tables in a TOML data source.

* *

All getters can fall back to default values if they have been provided as a constructor argument. * Getters for simple values (String, Date, etc.) will return null if no matching key exists. * {@link #getList(String)}, {@link #getTable(String)} and {@link #getTables(String)} return empty values if there is no matching key.

* *

All read methods throw an {@link IllegalStateException} if the TOML is incorrect.

* *

Example usage:

*

 * Toml toml = new Toml().read(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 { private static final Gson DEFAULT_GSON = new Gson(); @Getter private Map values = new HashMap(); private final Toml defaults; /** * Creates Toml instance with no defaults. */ public Toml() { this(null); } /** * @param defaults fallback values used when the requested key or table is not present in the TOML source that has been read. */ public Toml(Toml defaults) { this(defaults, new HashMap<>()); } /** * Populates the current Toml instance with values from file. * * @param file The File to be read. Expected to be encoded as UTF-8. * @return this instance * @throws IllegalStateException If file contains invalid TOML */ public Toml read(File file) { try { return read(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); } catch (Exception e) { throw new RuntimeException(e); } } /** * Populates the current Toml instance with values from inputStream. * * @param inputStream Closed after it has been read. * @return this instance * @throws IllegalStateException If file contains invalid TOML */ public Toml read(InputStream inputStream) { return read(new InputStreamReader(inputStream)); } /** * Populates the current Toml instance with values from reader. * * @param reader Closed after it has been read. * @return this instance * @throws IllegalStateException If file contains invalid TOML */ public Toml read(Reader reader) { BufferedReader bufferedReader = null; try { bufferedReader = new BufferedReader(reader); StringBuilder w = new StringBuilder(); String line = bufferedReader.readLine(); while (line != null) { w.append(line).append('\n'); line = bufferedReader.readLine(); } read(w.toString()); } catch (IOException e) { throw new RuntimeException(e); } finally { try { bufferedReader.close(); } catch (IOException e) { } } return this; } /** * Populates the current Toml instance with values from otherToml. * * @param otherToml * @return this instance */ public Toml read(Toml otherToml) { this.values = otherToml.values; return this; } /** * Populates the current Toml instance with values from tomlString. * * @param tomlString String to be read. * @return this instance * @throws IllegalStateException If tomlString is not valid TOML */ public Toml read(String tomlString) throws IllegalStateException { dev.plex.toml.Results results = dev.plex.toml.TomlParser.run(tomlString); if (results.errors.hasErrors()) { throw new IllegalStateException(results.errors.toString()); } this.values = results.consume(); return this; } public String getString(String key) { return (String) get(key); } public String getString(String key, String defaultValue) { String val = getString(key); return val == null ? defaultValue : val; } public Long getLong(String key) { return (Long) get(key); } public Long getLong(String key, Long defaultValue) { Long val = getLong(key); return val == null ? defaultValue : val; } /** * @param key a TOML key * @param type of list items * @return null if the key is not found */ public List getList(String key) { @SuppressWarnings("unchecked") List list = (List) get(key); return list; } /** * @param key a TOML key * @param defaultValue a list of default values * @param type of list items * @return null is the key is not found */ public List getList(String key, List defaultValue) { List list = getList(key); return list != null ? list : defaultValue; } public Boolean getBoolean(String key) { return (Boolean) get(key); } public Boolean getBoolean(String key, Boolean defaultValue) { Boolean val = getBoolean(key); return val == null ? defaultValue : val; } public Date getDate(String key) { return (Date) get(key); } public Date getDate(String key, Date defaultValue) { Date val = getDate(key); return val == null ? defaultValue : val; } public Double getDouble(String key) { return (Double) get(key); } public Double getDouble(String key, Double defaultValue) { Double val = getDouble(key); return val == null ? defaultValue : val; } /** * @param key A table name, not including square brackets. * @return A new Toml instance or null if no value is found for key. */ @SuppressWarnings("unchecked") public Toml getTable(String key) { Map map = (Map) get(key); return map != null ? new Toml(null, map) : null; } /** * @param key Name of array of tables, not including square brackets. * @return A {@link List} of Toml instances or null if no value is found for key. */ @SuppressWarnings("unchecked") public List getTables(String key) { List> tableArray = (List>) get(key); if (tableArray == null) { return null; } ArrayList tables = new ArrayList(); for (Map table : tableArray) { tables.add(new Toml(null, table)); } return tables; } /** * @param key a key name, can be compound (eg. a.b.c) * @return true if key is present */ public boolean contains(String key) { return get(key) != null; } /** * @param key a key name, can be compound (eg. a.b.c) * @return true if key is present and is a primitive */ public boolean containsPrimitive(String key) { Object object = get(key); return object != null && !(object instanceof Map) && !(object instanceof List); } /** * @param key a key name, can be compound (eg. a.b.c) * @return true if key is present and is a table */ public boolean containsTable(String key) { Object object = get(key); return object != null && (object instanceof Map); } /** * @param key a key name, can be compound (eg. a.b.c) * @return true if key is present and is a table array */ public boolean containsTableArray(String key) { Object object = get(key); return object != null && (object instanceof List); } public boolean isEmpty() { return values.isEmpty(); } /** *

* Populates an instance of targetClass with the values of this Toml instance. * The target's field names must match keys or tables. * Keys not present in targetClass will be ignored. *

* *

Tables are recursively converted to custom classes or to {@link Map Map<String, Object>}.

* *

In addition to straight-forward conversion of TOML primitives, the following are also available:

* *
    *
  • Integer -> int, long (or wrapper), {@link java.math.BigInteger}
  • *
  • Float -> float, double (or wrapper), {@link java.math.BigDecimal}
  • *
  • One-letter String -> char, {@link Character}
  • *
  • String -> {@link String}, enum, {@link java.net.URI}, {@link java.net.URL}
  • *
  • Multiline and Literal Strings -> {@link String}
  • *
  • Array -> {@link List}, {@link Set}, array. The generic type can be anything that can be converted.
  • *
  • Table -> Custom class, {@link Map Map<String, Object>}
  • *
* * @param targetClass Class to deserialize TOML to. * @param type of targetClass. * @return A new instance of targetClass. */ public T to(Class targetClass) { JsonElement json = DEFAULT_GSON.toJsonTree(toMap()); if (targetClass == JsonElement.class) { return targetClass.cast(json); } return DEFAULT_GSON.fromJson(json, targetClass); } public Map toMap() { HashMap valuesCopy = new HashMap(values); if (defaults != null) { for (Map.Entry entry : defaults.values.entrySet()) { if (!valuesCopy.containsKey(entry.getKey())) { valuesCopy.put(entry.getKey(), entry.getValue()); } } } return valuesCopy; } /** * @return a {@link Set} of Map.Entry instances. Modifications to the {@link Set} are not reflected in this Toml instance. Entries are immutable, so {@link Map.Entry#setValue(Object)} throws an UnsupportedOperationException. */ public Set> entrySet() { Set> entries = new LinkedHashSet>(); for (Map.Entry entry : values.entrySet()) { Class entryClass = entry.getValue().getClass(); if (Map.class.isAssignableFrom(entryClass)) { entries.add(new Entry(entry.getKey(), getTable(entry.getKey()))); } else if (List.class.isAssignableFrom(entryClass)) { List value = (List) entry.getValue(); if (!value.isEmpty() && value.get(0) instanceof Map) { entries.add(new Entry(entry.getKey(), getTables(entry.getKey()))); } else { entries.add(new Entry(entry.getKey(), value)); } } else { entries.add(new Entry(entry.getKey(), entry.getValue())); } } return entries; } private class Entry implements Map.Entry { private final String key; private final Object value; @Override public String getKey() { return key; } @Override public Object getValue() { return value; } @Override public Object setValue(Object value) { throw new UnsupportedOperationException("TOML entry values cannot be changed."); } private Entry(String key, Object value) { this.key = key; this.value = value; } } @SuppressWarnings("unchecked") public Object get(String key) { if (values.containsKey(key)) { return values.get(key); } Object current = new HashMap<>(values); dev.plex.toml.Keys.Key[] keys = dev.plex.toml.Keys.split(key); for (dev.plex.toml.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(k.name); if (k.index > -1 && current != null) { if (k.index >= ((List) current).size()) { return null; } current = ((List) current).get(k.index); } if (current == null) { return defaults != null ? defaults.get(key) : null; } } return current; } private Toml(Toml defaults, Map values) { this.values = values; this.defaults = defaults; } }