add toml support

This commit is contained in:
Looly 2023-03-28 02:04:37 +08:00
parent a30d5bb1a1
commit 42853b6706
9 changed files with 1159 additions and 18 deletions

View File

@ -25,7 +25,7 @@ public final class FastStringWriter extends Writer {
private static final int DEFAULT_CAPACITY = 16;
private final StringBuilder builder;
private final StringBuilder stringBuilder;
/**
* 构造
@ -43,31 +43,51 @@ public final class FastStringWriter extends Writer {
if (initialSize < 0) {
initialSize = DEFAULT_CAPACITY;
}
this.builder = new StringBuilder(initialSize);
this.stringBuilder = new StringBuilder(initialSize);
}
// region ----- append
@Override
public FastStringWriter append(final char c) {
this.stringBuilder.append(c);
return this;
}
@Override
public FastStringWriter append(final CharSequence csq, final int start, final int end) {
this.stringBuilder.append(csq, start, end);
return this;
}
@Override
public FastStringWriter append(final CharSequence csq) {
this.stringBuilder.append(csq);
return this;
}
// endregion
// region ----- write
@Override
public void write(final int c) {
this.builder.append((char) c);
this.stringBuilder.append((char) c);
}
@Override
public void write(final String str) {
this.builder.append(str);
this.stringBuilder.append(str);
}
@Override
public void write(final String str, final int off, final int len) {
this.builder.append(str, off, off + len);
this.stringBuilder.append(str, off, off + len);
}
@Override
public void write(final char[] cbuf) {
this.builder.append(cbuf, 0, cbuf.length);
this.stringBuilder.append(cbuf, 0, cbuf.length);
}
@ -79,8 +99,9 @@ public final class FastStringWriter extends Writer {
} else if (len == 0) {
return;
}
this.builder.append(cbuf, off, len);
this.stringBuilder.append(cbuf, off, len);
}
// endregion
@Override
@ -97,7 +118,7 @@ public final class FastStringWriter extends Writer {
@Override
public String toString() {
return this.builder.toString();
return this.stringBuilder.toString();
}
}

View File

@ -18,7 +18,7 @@ import cn.hutool.core.io.resource.NoResourceException;
import cn.hutool.cron.pattern.CronPattern;
import cn.hutool.cron.task.Task;
import cn.hutool.setting.Setting;
import cn.hutool.setting.SettingRuntimeException;
import cn.hutool.setting.SettingException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@ -58,7 +58,7 @@ public class CronUtil {
public static void setCronSetting(final String cronSettingPath) {
try {
crontabSetting = new Setting(cronSettingPath, Setting.DEFAULT_CHARSET, false);
} catch (final SettingRuntimeException | NoResourceException e) {
} catch (final SettingException | NoResourceException e) {
// ignore setting file parse error and no config error
}
}

View File

@ -43,10 +43,11 @@
<artifactId>hutool-log</artifactId>
<version>${project.parent.version}</version>
</dependency>
<!-- YAML支持 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.32</version>
<version>2.0</version>
<optional>true</optional>
</dependency>
</dependencies>

View File

@ -19,30 +19,30 @@ import cn.hutool.core.text.StrUtil;
*
* @author xiaoleilu
*/
public class SettingRuntimeException extends RuntimeException {
public class SettingException extends RuntimeException {
private static final long serialVersionUID = 7941096116780378387L;
public SettingRuntimeException(final Throwable e) {
public SettingException(final Throwable e) {
super(e);
}
public SettingRuntimeException(final String message) {
public SettingException(final String message) {
super(message);
}
public SettingRuntimeException(final String messageTemplate, final Object... params) {
public SettingException(final String messageTemplate, final Object... params) {
super(StrUtil.format(messageTemplate, params));
}
public SettingRuntimeException(final String message, final Throwable throwable) {
public SettingException(final String message, final Throwable throwable) {
super(message, throwable);
}
public SettingRuntimeException(final String message, final Throwable throwable, final boolean enableSuppression, final boolean writableStackTrace) {
public SettingException(final String message, final Throwable throwable, final boolean enableSuppression, final boolean writableStackTrace) {
super(message, throwable, enableSuppression, writableStackTrace);
}
public SettingRuntimeException(final Throwable throwable, final String messageTemplate, final Object... params) {
public SettingException(final Throwable throwable, final String messageTemplate, final Object... params) {
super(StrUtil.format(messageTemplate, params), throwable);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package cn.hutool.setting.toml;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
public class Toml {
/**
* A DateTimeFormatter that uses the TOML format.
*/
public static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_LOCAL_DATE)
.optionalStart()
.appendLiteral('T')
.append(DateTimeFormatter.ISO_LOCAL_TIME)
.optionalStart()
.appendOffsetId()
.optionalEnd()
.optionalEnd()
.toFormatter();
}

View File

@ -0,0 +1,676 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package cn.hutool.setting.toml;
import cn.hutool.setting.SettingException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.*;
/**
* TOML文件读取
* <h1>DateTimes support</h1>
* <p>
* The datetime support is more extended than in the TOML specification. This reader supports three kind of datetimes:
* <ol>
* <li>Full RFC 3339. Examples: 1979-05-27T07:32:00Z, 1979-05-27T00:32:00-07:00, 1979-05-27T00:32:00.999999-07:00</li>
* <li>Without local offset. Examples: 1979-05-27T07:32:00, 1979-05-27T00:32:00.999999</li>
* <li>Without time (just the date). Example: 2015-03-20</li>
* </ol>
* Moreover, parsing datetimes gives different objects according to the informations provided. For example, 2015-03-20
* is parsed as a {@link LocalDate}, 2015-03-20T19:04:35 as a {@link LocalDateTime}, and 2015-03-20T19:04:35+01:00 as a
* {@link ZonedDateTime}.
* </p>
* <h1>Lenient bare keys</h1>
* <p>
* This library allows "lenient" bare keys by default, as opposite to the "strict" bare keys required by the TOML
* specification. Strict bare keys may only contain letters, numbers, underscores, and dashes (A-Za-z0-9_-). Lenient
* bare keys may contain any character except those below the space character ' ' in the unicode table, '.', '[', ']'
* and '='. The behaviour of TomlReader regarding bare keys is set in its constructor.
* </p>
*
* @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
/**
* Creates a new TomlReader.
*
* @param data the TOML data to read
* @param strictAsciiBareKeys <code>true</false> to allow only strict bare keys, {@code false} to allow lenient ones.
*/
public TomlReader(final String data, final boolean strictAsciiBareKeys) {
this.data = data;
this.strictAsciiBareKeys = strictAsciiBareKeys;
}
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);
}
}
public Map<String, Object> read() {
final Map<String, Object> 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<String> 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<String, Object> value = nextTableContent();
// -- Saves the value --
Map<String, Object> 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<String, Object> childMap;
if (child == null) {// implicit table
childMap = new HashMap<>(4);
valueMap.put(part, childMap);
} else if (child instanceof Map) {// table
childMap = (Map) child;
} else {// array
final List<Map> 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<Map> 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 List nextArray() {
final ArrayList<Object> 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<String, Object> nextInlineTable() {
final Map<String, Object> map = new HashMap<>();
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<String, Object> nextTableContent() {
final Map<String, Object> map = new HashMap<>();
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);
}
}
}

View File

@ -0,0 +1,353 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
package cn.hutool.setting.toml;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharUtil;
import cn.hutool.setting.SettingException;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
/**
* Class for writing TOML v0.4.0.
* <h1>DateTimes support</h1>
* <p>
* Any {@link TemporalAccessor} may be added in a Map passed to this writer, this writer can only write three
* kind of datetimes: {@link LocalDate}, {@link LocalDateTime} and {@link ZonedDateTime}.
* </p>
* <h1>Lenient bare keys</h1>
* <p>
* The {@link TomlWriter} always outputs data that strictly follows the TOML specification. Any key that
* contains one
* or more non-strictly valid character is surrounded by quotes.
* </p>
*
* @author TheElectronWill
*/
public class TomlWriter {
private final Writer writer;
private final int indentSize;
private final char indentCharacter;
private final String lineSeparator;
private final LinkedList<String> tablesNames = new LinkedList<>();
private int lineBreaks = 0, indentationLevel = -1;// -1 to prevent indenting the first level
/**
* Creates a new TomlWriter with the defaults parameters. The system line separator is used (ie '\n' on
* Linux and OSX, "\r\n" on Windows). This is exactly the same as
* {@code TomlWriter(writer, 1, false, System.lineSeparator()}.
*
* @param writer where to write the data
*/
public TomlWriter(final Writer writer) {
this(writer, 1, false, System.lineSeparator());
}
/**
* Creates a new TomlWriter with the specified parameters. The system line separator is used (ie '\n' on
* Linux and OSX, "\r\n" on Windows). This is exactly the same as
* {@code TomlWriter(writer, indentSize, indentWithSpaces, System.lineSeparator())}.
*
* @param writer where to write the data
* @param indentSize the size of each indent
* @param indentWithSpaces true to indent with spaces, false to indent with tabs
*/
public TomlWriter(final Writer writer, final int indentSize, final boolean indentWithSpaces) {
this(writer, indentSize, indentWithSpaces, System.lineSeparator());
}
/**
* Creates a new TomlWriter with the specified parameters.
*
* @param writer where to write the data
* @param indentSize the size of each indent
* @param indentWithSpaces true to indent with spaces, false to indent with tabs
* @param lineSeparator the String to write to break lines
*/
public TomlWriter(final Writer writer, final int indentSize, final boolean indentWithSpaces, final String lineSeparator) {
this.writer = writer;
this.indentSize = indentSize;
this.indentCharacter = indentWithSpaces ? CharUtil.SPACE : CharUtil.TAB;
this.lineSeparator = lineSeparator;
}
/**
* Closes the underlying writer, flushing it first.
*
* @throws IOException if an error occurs
*/
public void close() throws IOException {
writer.close();
}
/**
* Flushes the underlying writer.
*
* @throws IOException if an error occurs
*/
public void flush() throws IOException {
writer.flush();
}
/**
* Writes the specified data in the TOML format.
*
* @param data the data to write
* @throws IOException if an error occurs
*/
public void write(final Map<String, Object> data) throws IOException {
writeTableContent(data);
}
private void writeTableName() throws IOException {
final Iterator<String> it = tablesNames.iterator();
while (it.hasNext()) {
final String namePart = it.next();
writeKey(namePart);
if (it.hasNext()) {
write('.');
}
}
}
private void writeTableContent(final Map<String, Object> table) throws IOException {
writeTableContent(table, true);
writeTableContent(table, false);
}
/**
* Writes the content of a table.
*
* @param table the table to write
* @param simpleValues true to write only the simple values (and the normal arrays), false to write only
* the tables
* (and the arrays of tables).
*/
@SuppressWarnings("unchecked")
private void writeTableContent(final Map<String, Object> table, final boolean simpleValues) throws IOException {
for (final Map.Entry<String, Object> entry : table.entrySet()) {
final String name = entry.getKey();
final Object value = entry.getValue();
if (value instanceof Collection) {// array
final Collection<?> c = (Collection<?>) value;
if (false == c.isEmpty() && c.iterator().next() instanceof Map) {// array of tables
if (simpleValues) {
continue;
}
tablesNames.addLast(name);
indentationLevel++;
for (final Object element : c) {
indent();
write("[[");
writeTableName();
write("]]\n");
final Map<String, Object> map = (Map<String, Object>) element;
writeTableContent(map);
}
indentationLevel--;
tablesNames.removeLast();
} else {// normal array
if (false == simpleValues) {
continue;
}
indent();
writeKey(name);
write(" = ");
writeArray(c);
}
} else if (value instanceof Object[]) {// array
final Object[] array = (Object[]) value;
if (array.length > 0 && array[0] instanceof Map) {// array of tables
if (simpleValues) {
continue;
}
tablesNames.addLast(name);
indentationLevel++;
for (final Object element : array) {
indent();
write("[[");
writeTableName();
write("]]\n");
final Map<String, Object> map = (Map<String, Object>) element;
writeTableContent(map);
}
indentationLevel--;
tablesNames.removeLast();
} else {// normal array
if (false == simpleValues) {
continue;
}
indent();
writeKey(name);
write(" = ");
writeString(ArrayUtil.toString(array));
}
} else if (value instanceof Map) {// table
if (simpleValues) {
continue;
}
tablesNames.addLast(name);
indentationLevel++;
indent();
write('[');
writeTableName();
write(']');
newLine();
writeTableContent((Map<String, Object>) value);
indentationLevel--;
tablesNames.removeLast();
} else {// simple value
if (!simpleValues) {
continue;
}
indent();
writeKey(name);
write(" = ");
writeValue(value);
}
newLine();
}
newLine();
}
private void writeKey(final String key) throws IOException {
for (int i = 0; i < key.length(); i++) {
final char c = key.charAt(i);
if (false == isValidCharOfKey(c)) {
// 含有非法字符包装之
writeString(key);
return;
}
}
write(key);
}
private static boolean isValidCharOfKey(final char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_';
}
private void writeString(final String str) throws IOException {
final StringBuilder sb = new StringBuilder();
sb.append('"');
for (int i = 0; i < str.length(); i++) {
final char c = str.charAt(i);
addEscaped(c, sb);
}
sb.append('"');
write(sb.toString());
}
private void writeArray(final Collection<?> c) throws IOException {
write('[');
for (final Object element : c) {
writeValue(element);
write(", ");
}
write(']');
}
private void writeValue(final Object value) throws IOException {
if (value instanceof String) {
writeString((String) value);
} else if (value instanceof Number || value instanceof Boolean) {
write(value.toString());
} else if (value instanceof TemporalAccessor) {
String formatted = Toml.DATE_FORMATTER.format((TemporalAccessor) value);
if (formatted.endsWith("T"))// If the last character is a 'T'
{
formatted = formatted.substring(0, formatted.length() - 1);// removes it because it's invalid.
}
write(formatted);
} else if (value instanceof Collection) {
writeArray((Collection<?>) value);
} else if (ArrayUtil.isArray(value)) {
write(ArrayUtil.toString(value));
} else if (value instanceof Map) {// should not happen because an array of tables is detected by
// writeTableContent()
throw new IOException("Unexpected value " + value);
} else {
throw new SettingException("Unsupported value of type " + value.getClass().getCanonicalName());
}
}
private void newLine() throws IOException {
if (lineBreaks <= 1) {
writer.write(lineSeparator);
lineBreaks++;
}
}
private void write(final char c) throws IOException {
writer.write(c);
lineBreaks = 0;
}
private void write(final String str) throws IOException {
writer.write(str);
lineBreaks = 0;
}
private void indent() throws IOException {
for (int i = 0; i < indentationLevel; i++) {
for (int j = 0; j < indentSize; j++) {
write(indentCharacter);
}
}
}
static void addEscaped(final char c, final StringBuilder sb) {
switch (c) {
case '\b':
sb.append("\\b");
break;
case '\t':
sb.append("\\t");
break;
case '\n':
sb.append("\\n");
break;
case '\\':
sb.append("\\\\");
break;
case '\r':
sb.append("\\r");
break;
case '\f':
sb.append("\\f");
break;
case '"':
sb.append("\\\"");
break;
default:
sb.append(c);
break;
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2023 looly(loolly@aliyun.com)
* Hutool is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* http://license.coscl.org.cn/MulanPSL2
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
/**
* TOMLTom's Obvious, Minimal Language配置文件解析和生成
*
* <p>
* 规范https://toml.io/cn/
* </p>
* <p>
* 参考实现https://github.com/TheElectronWill/TOML-javalib
* </p>
*
* @author looly
*/
package cn.hutool.setting.toml;

View File

@ -0,0 +1,33 @@
# This is a TOML document.
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[servers]
# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"
[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[clients]
data = [ ["gamma", "delta"], [1, 2] ]
# Line breaks are OK when inside arrays
hosts = [
"alpha",
"omega"
]