This commit is contained in:
Looly 2024-08-04 19:59:12 +08:00
parent 75f9a66998
commit 11ba7ba36c
4 changed files with 177 additions and 145 deletions

View File

@ -110,7 +110,7 @@ public class JSONParser {
}
default:
tokener.back();
key = nextValue(true).toString();
key = tokener.nextString();
}
// The key is followed by ':'.
@ -120,7 +120,7 @@ public class JSONParser {
throw tokener.syntaxError("Expected a ':' after a key");
}
jsonObject.set(key, nextValue(false), predicate);
jsonObject.set(key, nextValue(), predicate);
// Pairs are separated by ','.
@ -162,7 +162,7 @@ public class JSONParser {
jsonArray.addRaw(null, predicate);
} else {
x.back();
jsonArray.addRaw(nextValue(false), predicate);
jsonArray.addRaw(nextValue(), predicate);
}
switch (x.nextClean()) {
case CharUtil.COMMA:
@ -184,20 +184,19 @@ public class JSONParser {
/**
* 获得下一个值值类型可以是Boolean, Double, Integer, JSONArray, JSONObject, Long, or String
*
* @param getOnlyStringValue 是否只获取String值
* @return Boolean, Double, Integer, JSONArray, JSONObject, Long, or String
* @throws JSONException 语法错误
*/
public Object nextValue(final boolean getOnlyStringValue) throws JSONException {
return nextValue(getOnlyStringValue, (token, tokener, config) -> {
public Object nextValue() throws JSONException {
return nextValue((token, tokener, config) -> {
switch (token) {
case '{':
case CharUtil.DELIM_START:
try {
return new JSONObject(this, config);
} catch (final StackOverflowError e) {
throw new JSONException("JSONObject depth too large to process.", e);
}
case '[':
case CharUtil.BRACKET_START:
try {
return new JSONArray(this, config);
} catch (final StackOverflowError e) {
@ -211,45 +210,29 @@ public class JSONParser {
/**
* 获得下一个值值类型可以是Boolean, Double, Integer, JSONArray, JSONObject, Long, or String
*
* @param getOnlyStringValue 是否只获取String值
* @param objectBuilder JSON对象构建器
* @return Boolean, Double, Integer, JSONArray, JSONObject, Long, or String
* @throws JSONException 语法错误
*/
public Object nextValue(final boolean getOnlyStringValue, final ObjectBuilder objectBuilder) throws JSONException {
public Object nextValue(final ObjectBuilder objectBuilder) throws JSONException {
final JSONTokener tokener = this.tokener;
char c = tokener.nextClean();
final char c = tokener.nextClean();
switch (c) {
case '"':
case '\'':
case CharUtil.DOUBLE_QUOTES:
case CharUtil.SINGLE_QUOTE:
return tokener.nextString(c);
case '{':
case '[':
if (getOnlyStringValue) {
throw tokener.syntaxError("String value must not begin with '{'");
}
case CharUtil.DELIM_START:
case CharUtil.BRACKET_START:
tokener.back();
return objectBuilder.build(c, tokener, this.config);
}
/*
* Handle unquoted text. This could be the values true, false, or null, or it can be a number.
* An implementation (such as this one) is allowed to also accept non-standard forms. Accumulate
* characters until we reach the end of the text or a formatting character.
* 处理无引号包装的字符串 true, false, null, number.
* 同样兼容非标准的字符串如key无引号包装
* 此方法会不断读取并积累字符直到遇到token符
*/
final StringBuilder sb = new StringBuilder();
while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) {
sb.append(c);
c = tokener.next();
}
tokener.back();
final String valueString = sb.toString().trim();
if (valueString.isEmpty()) {
throw tokener.syntaxError("Missing value");
}
return getOnlyStringValue ? valueString : InternalJSONUtil.parseValueFromString(valueString);
return InternalJSONUtil.parseValueFromString(tokener.nextUnwrapString(c));
}
/**

View File

@ -16,6 +16,8 @@ import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.ReaderWrapper;
import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.math.NumberUtil;
import org.dromara.hutool.core.text.CharUtil;
import org.dromara.hutool.core.text.StrUtil;
import java.io.IOException;
import java.io.InputStream;
@ -23,12 +25,18 @@ import java.io.Reader;
import java.io.StringReader;
/**
* JSON解析器用于将JSON字符串解析为JSONObject或者JSONArray
* JSON解析器<br>
* 用于解析JSON字符串支持流式解析即逐个字符解析而不是一次性解析整个字符串
*
* @author from JSON.org
*/
public class JSONTokener extends ReaderWrapper {
/**
* JSON的分界符
*/
private static final String TOKENS = ",:]}/\\\"[{;=#";
/**
* 定义结束End of stream0
*/
@ -207,6 +215,20 @@ public class JSONTokener extends ReaderWrapper {
return chars;
}
/**
* 获取下一个token字符
*
* @return token字符
* @throws JSONException 非Token字符
*/
public char nextTokenChar() throws JSONException {
final char c = this.nextClean();
if (isNotTokenChar(c)) {
throw this.syntaxError("Invalid token char: " + c);
}
return c;
}
/**
* 获得下一个字符跳过空白符
*
@ -223,6 +245,59 @@ public class JSONTokener extends ReaderWrapper {
}
}
/**
* 读取一个字符串包括
* <ul>
* <li>使用引号包裹的字符串自动反转义</li>
* <li>无包装的字符串不转义</li>
* </ul>
*
* @return 截止到引号前的字符串
* @throws JSONException 出现无结束的字符串时抛出此异常
*/
public String nextString() throws JSONException {
final char c = nextClean();
switch (c) {
case CharUtil.DOUBLE_QUOTES:
case CharUtil.SINGLE_QUOTE:
return nextString(c);
}
// 兼容不严格的JSON如key不被双引号包围的情况
return nextUnwrapString(c);
}
/**
* 获得下一个字符串此字符串不以引号包围不会处理转义符主要解析
* <ul>
* <li>非严格的key无引号包围的key</li>
* <li>boolean值的字符串表示</li>
* <li>Number值的字符串表示</li>
* <li>null的字符串表示</li>
* </ul>
*
* @param c 首个字符
* @return 字符串
* @throws JSONException 读取空串时抛出此异常
*/
public String nextUnwrapString(char c) throws JSONException {
// 兼容不严格的JSON如key不被双引号包围的情况
final StringBuilder sb = new StringBuilder();
while (isNotTokenChar(c)) {
sb.append(c);
c = next();
}
if (c != EOF) {
back();
}
final String valueString = StrUtil.trim(sb);
if (valueString.isEmpty()) {
throw syntaxError("Missing value, maybe a token");
}
return valueString;
}
/**
* 返回当前位置到指定引号前的所有字符反斜杠的转义符也会被处理<br>
* 标准的JSON是不允许使用单引号包含字符串的但是此实现允许
@ -237,47 +312,21 @@ public class JSONTokener extends ReaderWrapper {
while (true) {
c = this.next();
switch (c) {
case 0:
case EOF:
throw this.syntaxError("Unterminated string");
case '\n':
case '\r':
case CharUtil.LF:
case CharUtil.CR:
//throw this.syntaxError("Unterminated string");
// https://gitee.com/dromara/hutool/issues/I76CSU
// 兼容非转义符
sb.append(c);
break;
case '\\':// 转义符
case CharUtil.BACKSLASH:// 转义符
c = this.next();
switch (c) {
case 'b':
sb.append('\b');
break;
case 't':
sb.append('\t');
break;
case 'n':
sb.append('\n');
break;
case 'f':
sb.append('\f');
break;
case 'r':
sb.append('\r');
break;
case 'u':// Unicode符
sb.append(nextUnicode());
break;
case '"':
case '\'':
case '\\':
case '/':
sb.append(c);
break;
default:
throw this.syntaxError("Illegal escape.");
}
sb.append(getUnescapeChar(c));
break;
default:
// 字符串结束
if (c == quote) {
return sb.toString();
}
@ -286,80 +335,6 @@ public class JSONTokener extends ReaderWrapper {
}
}
/**
* 获得从当前位置直到分隔符不包括分隔符或行尾的的所有字符
*
* @param delimiter 分隔符
* @return 字符串
* @throws JSONException JSON异常包装IO异常
*/
public String nextTo(final char delimiter) throws JSONException {
final StringBuilder sb = new StringBuilder();
for (; ; ) {
final char c = this.next();
if (c == delimiter || c == 0 || c == '\n' || c == '\r') {
if (c != 0) {
this.back();
}
return sb.toString().trim();
}
sb.append(c);
}
}
/**
* Get the text up but not including one of the specified delimiter characters or the end of line, whichever comes first.
*
* @param delimiters A set of delimiter characters.
* @return A string, trimmed.
* @throws JSONException JSON异常包装IO异常
*/
public String nextTo(final String delimiters) throws JSONException {
char c;
final StringBuilder sb = new StringBuilder();
for (; ; ) {
c = this.next();
if (delimiters.indexOf(c) >= 0 || c == 0 || c == '\n' || c == '\r') {
if (c != 0) {
this.back();
}
return sb.toString().trim();
}
sb.append(c);
}
}
/**
* Skip characters until the next character is the requested character. If the requested character is not found, no characters are skipped. 在遇到指定字符前跳过其它字符如果字符未找到则不跳过任何字符
*
* @param to 需要定位的字符
* @return 定位的字符如果字符未找到返回0
* @throws JSONException IO异常
*/
public char skipTo(final char to) throws JSONException {
char c;
try {
final long startIndex = this.index;
final long startCharacter = this.character;
final long startLine = this.line;
mark(1000000);
do {
c = this.next();
if (c == 0) {
reset();
this.index = startIndex;
this.character = startCharacter;
this.line = startLine;
return c;
}
} while (c != to);
} catch (final IOException e) {
throw new JSONException(e);
}
this.back();
return c;
}
/**
* Make a JSONException to signal a syntax error. <br>
* 构建 JSONException 用于表示语法错误
@ -380,4 +355,44 @@ public class JSONTokener extends ReaderWrapper {
public String toString() {
return " at " + this.index + " [character " + this.character + " line " + this.line + "]";
}
/**
* 获取反转义的字符
*
* @param c 转义的字符`\`后的字符
* @return 反转义字符
*/
private char getUnescapeChar(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 'u':// Unicode符
return nextUnicode();
case CharUtil.DOUBLE_QUOTES:
case CharUtil.SINGLE_QUOTE:
case CharUtil.BACKSLASH:
case CharUtil.SLASH:
return c;
default:
throw this.syntaxError("Illegal escape.");
}
}
/**
* 是否为可见的非Token字符这些字符存在于JSON的非字符串value中
*
* @param c char
* @return 是否为可见的非Token字符
*/
private static boolean isNotTokenChar(final char c) {
return c >= ' ' && TOKENS.indexOf(c) < 0;
}
}

View File

@ -183,7 +183,7 @@ public class JSONConverter implements Converter, Serializable {
final char firstC = jsonStr.charAt(0);
// RFC8259JSON字符串值number, boolean, or null
final JSONParser jsonParser = JSONParser.of(new JSONTokener(jsonStr), config);
final Object value = jsonParser.nextValue(false);
final Object value = jsonParser.nextValue();
if(jsonParser.getTokener().nextClean() != JSONTokener.EOF){
// 对于用户提供的未转义字符串导致解析未结束报错
throw new JSONException("JSON format error: {}", jsonStr);

View File

@ -12,14 +12,48 @@
package org.dromara.hutool.json;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class JSONTokenerTest {
@Test
public void parseTest() {
void parseTest() {
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.getUtf8Reader("issue1200.json"));
Assertions.assertNotNull(jsonObject);
assertNotNull(jsonObject);
}
@Test
void nextTest() {
final JSONTokener jsonTokener = new JSONTokener("{\"ab\": \"abc\"}");
final char c = jsonTokener.nextTokenChar();
assertEquals('{', c);
assertEquals("ab", jsonTokener.nextString());
final char c2 = jsonTokener.nextTokenChar();
assertEquals(':', c2);
assertEquals("abc", jsonTokener.nextString());
IoUtil.closeQuietly(jsonTokener);
}
/**
* 兼容非包装符包装的value和key
*/
@Test
void nextWithoutWrapperTest() {
final JSONTokener jsonTokener = new JSONTokener("{ab: abc}");
final char c = jsonTokener.nextTokenChar();
assertEquals('{', c);
assertEquals("ab", jsonTokener.nextString());
final char c2 = jsonTokener.nextTokenChar();
assertEquals(':', c2);
assertEquals("abc", jsonTokener.nextString());
IoUtil.closeQuietly(jsonTokener);
}
}