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

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.io.ReaderWrapper;
import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.lang.Assert;
import org.dromara.hutool.core.math.NumberUtil; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -23,12 +25,18 @@ import java.io.Reader;
import java.io.StringReader; import java.io.StringReader;
/** /**
* JSON解析器用于将JSON字符串解析为JSONObject或者JSONArray * JSON解析器<br>
* 用于解析JSON字符串支持流式解析即逐个字符解析而不是一次性解析整个字符串
* *
* @author from JSON.org * @author from JSON.org
*/ */
public class JSONTokener extends ReaderWrapper { public class JSONTokener extends ReaderWrapper {
/**
* JSON的分界符
*/
private static final String TOKENS = ",:]}/\\\"[{;=#";
/** /**
* 定义结束End of stream0 * 定义结束End of stream0
*/ */
@ -207,6 +215,20 @@ public class JSONTokener extends ReaderWrapper {
return chars; 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> * 返回当前位置到指定引号前的所有字符反斜杠的转义符也会被处理<br>
* 标准的JSON是不允许使用单引号包含字符串的但是此实现允许 * 标准的JSON是不允许使用单引号包含字符串的但是此实现允许
@ -237,47 +312,21 @@ public class JSONTokener extends ReaderWrapper {
while (true) { while (true) {
c = this.next(); c = this.next();
switch (c) { switch (c) {
case 0: case EOF:
throw this.syntaxError("Unterminated string"); throw this.syntaxError("Unterminated string");
case '\n': case CharUtil.LF:
case '\r': case CharUtil.CR:
//throw this.syntaxError("Unterminated string"); //throw this.syntaxError("Unterminated string");
// https://gitee.com/dromara/hutool/issues/I76CSU // https://gitee.com/dromara/hutool/issues/I76CSU
// 兼容非转义符 // 兼容非转义符
sb.append(c); sb.append(c);
break; break;
case '\\':// 转义符 case CharUtil.BACKSLASH:// 转义符
c = this.next(); c = this.next();
switch (c) { sb.append(getUnescapeChar(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.");
}
break; break;
default: default:
// 字符串结束
if (c == quote) { if (c == quote) {
return sb.toString(); 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> * Make a JSONException to signal a syntax error. <br>
* 构建 JSONException 用于表示语法错误 * 构建 JSONException 用于表示语法错误
@ -380,4 +355,44 @@ public class JSONTokener extends ReaderWrapper {
public String toString() { public String toString() {
return " at " + this.index + " [character " + this.character + " line " + this.line + "]"; 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); final char firstC = jsonStr.charAt(0);
// RFC8259JSON字符串值number, boolean, or null // RFC8259JSON字符串值number, boolean, or null
final JSONParser jsonParser = JSONParser.of(new JSONTokener(jsonStr), config); 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){ if(jsonParser.getTokener().nextClean() != JSONTokener.EOF){
// 对于用户提供的未转义字符串导致解析未结束报错 // 对于用户提供的未转义字符串导致解析未结束报错
throw new JSONException("JSON format error: {}", jsonStr); throw new JSONException("JSON format error: {}", jsonStr);

View File

@ -12,14 +12,48 @@
package org.dromara.hutool.json; package org.dromara.hutool.json;
import org.dromara.hutool.core.io.IoUtil;
import org.dromara.hutool.core.io.resource.ResourceUtil; import org.dromara.hutool.core.io.resource.ResourceUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; 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 { public class JSONTokenerTest {
@Test @Test
public void parseTest() { void parseTest() {
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.getUtf8Reader("issue1200.json")); 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);
} }
} }