mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-04-19 03:01:48 +08:00
add toml support
This commit is contained in:
parent
a30d5bb1a1
commit
42853b6706
@ -25,7 +25,7 @@ public final class FastStringWriter extends Writer {
|
|||||||
|
|
||||||
private static final int DEFAULT_CAPACITY = 16;
|
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) {
|
if (initialSize < 0) {
|
||||||
initialSize = DEFAULT_CAPACITY;
|
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
|
@Override
|
||||||
public void write(final int c) {
|
public void write(final int c) {
|
||||||
this.builder.append((char) c);
|
this.stringBuilder.append((char) c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(final String str) {
|
public void write(final String str) {
|
||||||
this.builder.append(str);
|
this.stringBuilder.append(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(final String str, final int off, final int len) {
|
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
|
@Override
|
||||||
public void write(final char[] cbuf) {
|
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) {
|
} else if (len == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.builder.append(cbuf, off, len);
|
this.stringBuilder.append(cbuf, off, len);
|
||||||
}
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -97,7 +118,7 @@ public final class FastStringWriter extends Writer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return this.builder.toString();
|
return this.stringBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import cn.hutool.core.io.resource.NoResourceException;
|
|||||||
import cn.hutool.cron.pattern.CronPattern;
|
import cn.hutool.cron.pattern.CronPattern;
|
||||||
import cn.hutool.cron.task.Task;
|
import cn.hutool.cron.task.Task;
|
||||||
import cn.hutool.setting.Setting;
|
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.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@ -58,7 +58,7 @@ public class CronUtil {
|
|||||||
public static void setCronSetting(final String cronSettingPath) {
|
public static void setCronSetting(final String cronSettingPath) {
|
||||||
try {
|
try {
|
||||||
crontabSetting = new Setting(cronSettingPath, Setting.DEFAULT_CHARSET, false);
|
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
|
// ignore setting file parse error and no config error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,10 +43,11 @@
|
|||||||
<artifactId>hutool-log</artifactId>
|
<artifactId>hutool-log</artifactId>
|
||||||
<version>${project.parent.version}</version>
|
<version>${project.parent.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- YAML支持 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.yaml</groupId>
|
<groupId>org.yaml</groupId>
|
||||||
<artifactId>snakeyaml</artifactId>
|
<artifactId>snakeyaml</artifactId>
|
||||||
<version>1.32</version>
|
<version>2.0</version>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -19,30 +19,30 @@ import cn.hutool.core.text.StrUtil;
|
|||||||
*
|
*
|
||||||
* @author xiaoleilu
|
* @author xiaoleilu
|
||||||
*/
|
*/
|
||||||
public class SettingRuntimeException extends RuntimeException {
|
public class SettingException extends RuntimeException {
|
||||||
private static final long serialVersionUID = 7941096116780378387L;
|
private static final long serialVersionUID = 7941096116780378387L;
|
||||||
|
|
||||||
public SettingRuntimeException(final Throwable e) {
|
public SettingException(final Throwable e) {
|
||||||
super(e);
|
super(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingRuntimeException(final String message) {
|
public SettingException(final String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingRuntimeException(final String messageTemplate, final Object... params) {
|
public SettingException(final String messageTemplate, final Object... params) {
|
||||||
super(StrUtil.format(messageTemplate, 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);
|
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);
|
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);
|
super(StrUtil.format(messageTemplate, params), throwable);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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();
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOML(Tom'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;
|
33
hutool-setting/src/test/resources/test.toml
Normal file
33
hutool-setting/src/test/resources/test.toml
Normal 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"
|
||||||
|
]
|
Loading…
x
Reference in New Issue
Block a user