All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.dromara.hutool.setting.toml.TomlReader Maven / Gradle / Ivy
/*
* Copyright (c) 2013-2024 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.setting.toml;
import org.dromara.hutool.setting.SettingException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.*;
/**
* TOML文件读取
* 来自:https://github.com/TheElectronWill/TOML-javalib
*
* 日期格式支持:
*
* 2015-03-20 转为:{@link LocalDate}
* 2015-03-20T19:04:35 转为:{@link LocalDateTime}
* 2015-03-20T19:04:35+01:00 转为:{@link ZonedDateTime}
*
*
* 此类支持更加宽松的key,除了{@code A-Za-z0-9_- },还支持' ','.', '[', ']' 和 '='
*
* @author TheElectronWill
*/
public class TomlReader {
private final String data;
private final boolean strictAsciiBareKeys;
private int pos = 0;// current position
private int line = 1;// current line
/**
* 构造
*
* 严格模式:[A-Za-z0-9_-]
* 宽松模式:所有字符但是不包括. [ ] # =
*
*
* @param data TOML数据
* @param strictAsciiBareKeys {@code true} 只允许严格的key格式, {@code false} 支持宽松格式.
*/
public TomlReader(final String data, final boolean strictAsciiBareKeys) {
this.data = data;
this.strictAsciiBareKeys = strictAsciiBareKeys;
}
/**
* 读取TOML
*
* @return TOML
*/
@SuppressWarnings("unchecked")
public Map read() {
final Map map = nextTableContent();
if (!hasNext() && pos > 0 && data.charAt(pos - 1) == '['){
throw new SettingException("Invalid table declaration at line " + line + ": it never ends");
}
while (hasNext()) {
char c = nextUseful(true);
final boolean twoBrackets;
if (c == '[') {
twoBrackets = true;
c = nextUseful(false);
} else {
twoBrackets = false;
}
pos--;
// --- Reads the key --
final List keyParts = new ArrayList<>(4);
boolean insideSquareBrackets = true;
while (insideSquareBrackets) {
if (!hasNext())
throw new SettingException("Invalid table declaration at line " + line + ": it never ends");
String name = null;
final char nameFirstChar = nextUseful(false);
switch (nameFirstChar) {
case '"': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '"' && c3 == '"') {
pos += 2;
name = nextBasicMultilineString();
}
}
if (name == null) {
name = nextBasicString();
}
break;
}
case '\'': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '\'' && c3 == '\'') {
pos += 2;
name = nextLiteralMultilineString();
}
}
if (name == null) {
name = nextLiteralString();
}
break;
}
default:
pos--;// to include the first (already read) non-space character
name = nextBareKey(']', '.').trim();
if (data.charAt(pos) == ']') {
if (!name.isEmpty())
keyParts.add(name);
insideSquareBrackets = false;
} else if (name.isEmpty()) {
throw new SettingException("Invalid empty key at line " + line);
}
pos++;// to go after the character we stopped at in nextBareKey()
break;
}
if (insideSquareBrackets)
keyParts.add(name.trim());
}
// -- Checks --
if (keyParts.isEmpty())
throw new SettingException("Invalid empty key at line " + line);
if (twoBrackets && next() != ']') {// 2 brackets at the start but only one at the end!
throw new SettingException("Missing character ']' at line " + line);
}
// -- Reads the value (table content) --
final Map value = nextTableContent();
// -- Saves the value --
Map valueMap = map;// the map that contains the value
for (int i = 0; i < keyParts.size() - 1; i++) {
final String part = keyParts.get(i);
final Object child = valueMap.get(part);
final Map childMap;
if (child == null) {// implicit table
childMap = new LinkedHashMap<>(4);
valueMap.put(part, childMap);
} else if (child instanceof Map) {// table
childMap = (Map) child;
} else {// array
final List> list = (List>) child;
childMap = list.get(list.size() - 1);
}
valueMap = childMap;
}
if (twoBrackets) {// element of a table array
final String name = keyParts.get(keyParts.size() - 1);
Collection> tableArray = (Collection>) valueMap.get(name);
if (tableArray == null) {
tableArray = new ArrayList<>(2);
valueMap.put(name, tableArray);
}
tableArray.add(value);
} else {// just a table
valueMap.put(keyParts.get(keyParts.size() - 1), value);
}
}
return map;
}
private boolean hasNext() {
return pos < data.length();
}
private char next() {
return data.charAt(pos++);
}
private char nextUseful(final boolean skipComments) {
char c = ' ';
while (hasNext() && (c == ' ' || c == '\t' || c == '\r' || c == '\n' || (c == '#' && skipComments))) {
c = next();
if (skipComments && c == '#') {
final int nextLinebreak = data.indexOf('\n', pos);
if (nextLinebreak == -1) {
pos = data.length();
} else {
pos = nextLinebreak + 1;
line++;
}
} else if (c == '\n') {
line++;
}
}
return c;
}
private char nextUsefulOrLinebreak() {
char c = ' ';
while (c == ' ' || c == '\t' || c == '\r') {
if (!hasNext())// fixes error when no '\n' at the end of the file
return '\n';
c = next();
}
if (c == '\n')
line++;
return c;
}
private Object nextValue(final char firstChar) {
switch (firstChar) {
case '+':
case '-':
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
return nextNumberOrDate(firstChar);
case '"':
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '"' && c3 == '"') {
pos += 2;
return nextBasicMultilineString();
}
}
return nextBasicString();
case '\'':
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '\'' && c3 == '\'') {
pos += 2;
return nextLiteralMultilineString();
}
}
return nextLiteralString();
case '[':
return nextArray();
case '{':
return nextInlineTable();
case 't':// Must be "true"
if (pos + 3 > data.length() || next() != 'r' || next() != 'u' || next() != 'e') {
throw new SettingException("Invalid value at line " + line);
}
return true;
case 'f':// Must be "false"
if (pos + 4 > data.length() || next() != 'a' || next() != 'l' || next() != 's' || next() != 'e') {
throw new SettingException("Invalid value at line " + line);
}
return false;
default:
throw new SettingException("Invalid character '" + toString(firstChar) + "' at line " + line);
}
}
private List nextArray() {
final ArrayList list = new ArrayList<>();
while (true) {
final char c = nextUseful(true);
if (c == ']') {
pos++;
break;
}
final Object value = nextValue(c);
if (!list.isEmpty() && !(list.get(0).getClass().isAssignableFrom(value.getClass())))
throw new SettingException("Invalid array at line " + line + ": all the values must have the same type");
list.add(value);
final char afterEntry = nextUseful(true);
if (afterEntry == ']') {
pos++;
break;
}
if (afterEntry != ',') {
throw new SettingException("Invalid array at line " + line + ": expected a comma after each value");
}
}
pos--;
list.trimToSize();
return list;
}
private Map nextInlineTable() {
final Map map = new LinkedHashMap<>();
while (true) {
final char nameFirstChar = nextUsefulOrLinebreak();
String name = null;
switch (nameFirstChar) {
case '}':
return map;
case '"': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '"' && c3 == '"') {
pos += 2;
name = nextBasicMultilineString();
}
}
if (name == null)
name = nextBasicString();
break;
}
case '\'': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '\'' && c3 == '\'') {
pos += 2;
name = nextLiteralMultilineString();
}
}
if (name == null)
name = nextLiteralString();
break;
}
default:
pos--;// to include the first (already read) non-space character
name = nextBareKey(' ', '\t', '=');
if (name.isEmpty())
throw new SettingException("Invalid empty key at line " + line);
break;
}
final char separator = nextUsefulOrLinebreak();// tries to find the '=' sign
if (separator != '=')
throw new SettingException("Invalid character '" + toString(separator) + "' at line " + line + ": expected '='");
final char valueFirstChar = nextUsefulOrLinebreak();
final Object value = nextValue(valueFirstChar);
map.put(name, value);
final char after = nextUsefulOrLinebreak();
if (after == '}' || !hasNext()) {
return map;
} else if (after != ',') {
throw new SettingException("Invalid inline table at line " + line + ": missing comma");
}
}
}
private Map nextTableContent() {
final Map map = new LinkedHashMap<>();
while (true) {
final char nameFirstChar = nextUseful(true);
if (!hasNext() || nameFirstChar == '[') {
return map;
}
String name = null;
switch (nameFirstChar) {
case '"': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '"' && c3 == '"') {
pos += 2;
name = nextBasicMultilineString();
}
}
if (name == null) {
name = nextBasicString();
}
break;
}
case '\'': {
if (pos + 1 < data.length()) {
final char c2 = data.charAt(pos);
final char c3 = data.charAt(pos + 1);
if (c2 == '\'' && c3 == '\'') {
pos += 2;
name = nextLiteralMultilineString();
}
}
if (name == null) {
name = nextLiteralString();
}
break;
}
default:
pos--;// to include the first (already read) non-space character
name = nextBareKey(' ', '\t', '=');
if (name.isEmpty())
throw new SettingException("Invalid empty key at line " + line);
break;
}
final char separator = nextUsefulOrLinebreak();// tries to find the '=' sign
if (separator != '=')// an other character
throw new SettingException("Invalid character '" + toString(separator) + "' at line " + line + ": expected '='");
final char valueFirstChar = nextUsefulOrLinebreak();
if (valueFirstChar == '\n') {
throw new SettingException("Invalid newline before the value at line " + line);
}
final Object value = nextValue(valueFirstChar);
final char afterEntry = nextUsefulOrLinebreak();
if (afterEntry == '#') {
pos--;// to make the next nextUseful() call read the # character
} else if (afterEntry != '\n') {
throw new SettingException("Invalid character '" + toString(afterEntry) + "' after the value at line " + line);
}
if (map.containsKey(name))
throw new SettingException("Duplicate key \"" + name + "\"");
map.put(name, value);
}
}
private Object nextNumberOrDate(final char first) {
boolean maybeDouble = true, maybeInteger = true, maybeDate = true;
final StringBuilder sb = new StringBuilder();
sb.append(first);
char c;
whileLoop:
while (hasNext()) {
c = next();
switch (c) {
case ':':
case 'T':
case 'Z':
maybeInteger = maybeDouble = false;
break;
case 'e':
case 'E':
maybeInteger = maybeDate = false;
break;
case '.':
maybeInteger = false;
break;
case '-':
if (pos != 0 && data.charAt(pos - 1) != 'e' && data.charAt(pos - 1) != 'E')
maybeInteger = maybeDouble = false;
break;
case ',':
case ' ':
case '\t':
case '\n':
case '\r':
case ']':
case '}':
pos--;
break whileLoop;
}
if (c == '_')
maybeDate = false;
else
sb.append(c);
}
final String valueStr = sb.toString();
try {
if (maybeInteger) {
if (valueStr.length() < 10)
return Integer.parseInt(valueStr);
return Long.parseLong(valueStr);
}
if (maybeDouble)
return Double.parseDouble(valueStr);
if (maybeDate)
return Toml.DATE_FORMATTER.parseBest(valueStr, ZonedDateTime::from, LocalDateTime::from, LocalDate::from);
} catch (final Exception ex) {
throw new SettingException("Invalid value: \"" + valueStr + "\" at line " + line, ex);
}
throw new SettingException("Invalid value: \"" + valueStr + "\" at line " + line);
}
private String nextBareKey(final char... allowedEnds) {
final String keyName;
for (int i = pos; i < data.length(); i++) {
final char c = data.charAt(i);
for (final char allowedEnd : allowedEnds) {
if (c == allowedEnd) {// checks if this character allowed to end this bare key
keyName = data.substring(pos, i);
pos = i;
return keyName;
}
}
if (strictAsciiBareKeys) {
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'))
throw new SettingException("Forbidden character '" + toString(c) + "' in strict bare-key at line " + line);
} else if (c <= ' ' || c == '#' || c == '=' || c == '.' || c == '[' || c == ']') {// lenient bare key
throw new SettingException("Forbidden character '" + toString(c) + "' in lenient bare-key at line " + line);
} // else continue reading
}
throw new SettingException(
"Invalid key/value pair at line " + line + " end of data reached before the value attached to the key was found");
}
private String nextLiteralString() {
final int index = data.indexOf('\'', pos);
if (index == -1)
throw new SettingException("Invalid literal String at line " + line + ": it never ends");
final String str = data.substring(pos, index);
if (str.indexOf('\n') != -1)
throw new SettingException("Invalid literal String at line " + line + ": newlines are not allowed here");
pos = index + 1;
return str;
}
private String nextLiteralMultilineString() {
final int index = data.indexOf("'''", pos);
if (index == -1)
throw new SettingException("Invalid multiline literal String at line " + line + ": it never ends");
final String str;
if (data.charAt(pos) == '\r' && data.charAt(pos + 1) == '\n') {// "\r\n" at the beginning of the string
str = data.substring(pos + 2, index);
line++;
} else if (data.charAt(pos) == '\n') {// '\n' at the beginning of the string
str = data.substring(pos + 1, index);
line++;
} else {
str = data.substring(pos, index);
}
for (int i = 0; i < str.length(); i++) {// count lines
final char c = str.charAt(i);
if (c == '\n')
line++;
}
pos = index + 3;// goes after the 3 quotes
return str;
}
private String nextBasicString() {
final StringBuilder sb = new StringBuilder();
boolean escape = false;
while (hasNext()) {
final char c = next();
if (c == '\n' || c == '\r')
throw new SettingException("Invalid basic String at line " + line + ": newlines not allowed");
if (escape) {
sb.append(unescape(c));
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == '"') {
return sb.toString();
} else {
sb.append(c);
}
}
throw new SettingException("Invalid basic String at line " + line + ": it nerver ends");
}
private String nextBasicMultilineString() {
final StringBuilder sb = new StringBuilder();
boolean first = true, escape = false;
while (hasNext()) {
final char c = next();
if (first && (c == '\r' || c == '\n')) {
if (c == '\r' && hasNext() && data.charAt(pos) == '\n')// "\r\n"
pos++;// so that it is NOT read by the next call to next()
else
line++;
first = false;
continue;
}
if (escape) {
if (c == '\r' || c == '\n' || c == ' ' || c == '\t') {
if (c == '\r' && hasNext() && data.charAt(pos) == '\n')// "\r\n"
pos++;
else if (c == '\n')
line++;
nextUseful(false);
pos--;// so that it is read by the next call to next()
} else {
sb.append(unescape(c));
}
escape = false;
} else if (c == '\\') {
escape = true;
} else if (c == '"') {
if (pos + 1 >= data.length())
break;
if (data.charAt(pos) == '"' && data.charAt(pos + 1) == '"') {
pos += 2;
return sb.toString();
}
} else if (c == '\n') {
line++;
sb.append(c);
} else {
sb.append(c);
}
}
throw new SettingException("Invalid multiline basic String at line " + line + ": it never ends");
}
private char unescape(final char c) {
switch (c) {
case 'b':
return '\b';
case 't':
return '\t';
case 'n':
return '\n';
case 'f':
return '\f';
case 'r':
return '\r';
case '"':
return '"';
case '\\':
return '\\';
case 'u': {// unicode uXXXX
if (data.length() - pos < 5)
throw new SettingException("Invalid unicode code point at line " + line);
final String unicode = data.substring(pos, pos + 4);
pos += 4;
try {
final int hexVal = Integer.parseInt(unicode, 16);
return (char) hexVal;
} catch (final NumberFormatException ex) {
throw new SettingException("Invalid unicode code point at line " + line, ex);
}
}
case 'U': {// unicode UXXXXXXXX
if (data.length() - pos < 9)
throw new SettingException("Invalid unicode code point at line " + line);
final String unicode = data.substring(pos, pos + 8);
pos += 8;
try {
final int hexVal = Integer.parseInt(unicode, 16);
return (char) hexVal;
} catch (final NumberFormatException ex) {
throw new SettingException("Invalid unicode code point at line " + line, ex);
}
}
default:
throw new SettingException("Invalid escape sequence: \"\\" + c + "\" at line " + line);
}
}
/**
* Converts a char to a String. The char is escaped if needed.
*/
private String toString(final char c) {
switch (c) {
case '\b':
return "\\b";
case '\t':
return "\\t";
case '\n':
return "\\n";
case '\r':
return "\\r";
case '\f':
return "\\f";
default:
return String.valueOf(c);
}
}
}