All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dromara.hutool.setting.toml.TomlReader Maven / Gradle / Ivy

There is a newer version: 6.0.0.M3
Show newest version
/*
 * 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); } } }