You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2020/10/13 19:57:41 UTC
[freemarker] 02/02: Forward ported from 2.3-gae: Added ?eval_json
to evaluate JSON given as flat string. This was added as ?eval is routinely
misused for the same purpose.
This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 3
in repository https://gitbox.apache.org/repos/asf/freemarker.git
commit 18cfeb4aa85916d80d961b7c2fe15b1cb3bcaf00
Author: ddekany <dd...@apache.org>
AuthorDate: Tue Oct 13 21:56:31 2020 +0200
Forward ported from 2.3-gae: Added ?eval_json to evaluate JSON given as flat string. This was added as ?eval is routinely misused for the same purpose.
---
.../freemarker/core/EvalJsonBuiltInTest.java | 36 ++
.../org/apache/freemarker/core/JSONParserTest.java | 170 ++++++
.../org/apache/freemarker/core/ASTExpBuiltIn.java | 4 +-
.../freemarker/core/BuiltInsForStringsMisc.java | 38 +-
.../org/apache/freemarker/core/JSONParser.java | 616 +++++++++++++++++++++
5 files changed, 852 insertions(+), 12 deletions(-)
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/EvalJsonBuiltInTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EvalJsonBuiltInTest.java
new file mode 100644
index 0000000..0c733be
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EvalJsonBuiltInTest.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.freemarker.core;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class EvalJsonBuiltInTest extends TemplateTest {
+
+ @Test
+ public void test() throws Exception {
+ assertOutput("${'1'?evalJson}", "1");
+
+ assertOutput("${'null'?evalJson!'-'}", "-");
+
+ assertOutput("<#list '{\"a\": 1e2, \"b\": null}'?evalJson as k, v>${k}=${v!'NULL'}<#sep>, </#list>", "a=100, b=NULL");
+ }
+
+}
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/JSONParserTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/JSONParserTest.java
new file mode 100644
index 0000000..4336d2b
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/JSONParserTest.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.DeepUnwrap;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class JSONParserTest {
+
+ @Test
+ public void testObjects() throws JSONParser.JSONParseException {
+ assertEquals(ImmutableMap.of("a", 1, "b", 2), JSONParser.parse("{\"a\": 1, \"b\": 2}"));
+ assertEquals(Collections.emptyMap(), JSONParser.parse("{}"));
+ try {
+ JSONParser.parse("{1: 1}");
+ fail();
+ } catch (JSONParser.JSONParseException e) {
+ assertThat(e.getMessage(), containsString("string key"));
+ }
+ }
+
+ @Test
+ public void testLists() throws JSONParser.JSONParseException {
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("[1, 2]"));
+ assertEquals(Collections.emptyList(), JSONParser.parse("[]"));
+ }
+
+ @Test
+ public void testStrings() throws JSONParser.JSONParseException {
+ assertEquals("", JSONParser.parse("\"\""));
+ assertEquals(" ", JSONParser.parse("\" \""));
+ assertEquals("'", JSONParser.parse("\"'\""));
+ assertEquals("foo", JSONParser.parse("\"foo\""));
+ assertEquals("\" \\ / \b \f \n \r \t \ufeff",
+ JSONParser.parse(
+ "\"" +
+ "\\\" \\\\ \\/ \\b \\f \\n \\r \\t \\uFEFF" +
+ "\""));
+ }
+
+ @Test
+ public void testNumbers() throws JSONParser.JSONParseException {
+ assertEquals(0, JSONParser.parse("0"));
+ assertEquals(123, JSONParser.parse("123"));
+ assertEquals(-123, JSONParser.parse("-123"));
+ assertNotEquals(123L, JSONParser.parse("123"));
+ assertEquals(2147483647, JSONParser.parse("2147483647"));
+ assertEquals(2147483648L, JSONParser.parse("2147483648"));
+ assertEquals(-2147483648, JSONParser.parse("-2147483648"));
+ assertEquals(-2147483649L, JSONParser.parse("-2147483649"));
+ assertEquals(-123, JSONParser.parse("-1.23E2"));
+ assertEquals(new BigDecimal("1.23"), JSONParser.parse("1.23"));
+ assertEquals(new BigDecimal("-1.23"), JSONParser.parse("-1.23"));
+ assertEquals(new BigDecimal("12.3"), JSONParser.parse("1.23E1"));
+ assertEquals(new BigDecimal("0.123"), JSONParser.parse("123E-3"));
+ }
+
+ @Test
+ public void testKeywords() throws JSONParser.JSONParseException {
+ assertEquals(null, JSONParser.parse("null"));
+ assertEquals(true, JSONParser.parse("true"));
+ assertEquals(false, JSONParser.parse("false"));
+ try {
+ JSONParser.parse("NULL");
+ fail();
+ } catch (JSONParser.JSONParseException e) {
+ assertThat(e.getMessage(), containsString("quoted"));
+ }
+ }
+
+ @Test
+ public void testBlockComments() throws JSONParser.JSONParseException {
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("/**/[/**/1/**/, /**/2/**/]/**/"));
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("/*x*/[/*x*/1/*x*/, /*x*/2/*x*/]/*x*/"));
+ assertEquals(ImmutableList.of(1), JSONParser.parse(" /*x*/ /**//**/ [ /*x*/ /*\n*//***/ 1 ]"));
+ try {
+ JSONParser.parse("/*");
+ fail();
+ } catch (JSONParser.JSONParseException e) {
+ assertThat(e.getMessage(), containsString("Unclosed comment"));
+ }
+ try {
+ JSONParser.parse("[/*]");
+ fail();
+ } catch (JSONParser.JSONParseException e) {
+ assertThat(e.getMessage(), containsString("Unclosed comment"));
+ }
+ }
+
+ @Test
+ public void testLineComments() throws JSONParser.JSONParseException {
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("//c1\n[ //c2\n1, //c3\n 2//c5\n] //c4"));
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("// c1\n//\r// c2\r\n// c3\r\n[ 1, 2 ]//"));
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("[1, 2]\n//\n"));
+ }
+
+ @Test
+ public void testWhitespace() throws JSONParser.JSONParseException {
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse(" [ 1 ,\n2 ] "));
+ assertEquals(ImmutableList.of(1, 2), JSONParser.parse("\uFEFF[\u00A01\u00A0,2]"));
+ }
+
+ @Test
+ public void testMixed() throws JSONParser.JSONParseException {
+ LinkedHashMap<String, Object> m = new LinkedHashMap<>();
+ m.put("x", 1);
+ m.put("y", null);
+ assertEquals(
+ ImmutableList.of(
+ ImmutableMap.of("a", Collections.emptyMap()),
+ ImmutableMap.of("b",
+ Arrays.asList(
+ m,
+ true,
+ null
+ ))
+ ),
+ JSONParser.parse("" +
+ "[\n" +
+ "{\"a\":{}},\n" +
+ "{\"b\":\n" +
+ "[" +
+ "{\"x\":1, \"y\": null}," +
+ "true," +
+ "null" +
+ "] // comment\n" +
+ "}\n" +
+ "]"));
+ }
+
+ private static void assertEquals(Object expected, TemplateModel actual) {
+ try {
+ Assert.assertEquals(expected, DeepUnwrap.unwrap(actual));
+ } catch (TemplateException e) {
+ throw new BugException(e);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
index 4102f3e..8500d6d 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltIn.java
@@ -61,6 +61,7 @@ import org.apache.freemarker.core.BuiltInsForSequences.seq_index_ofBI;
import org.apache.freemarker.core.BuiltInsForSequences.sortBI;
import org.apache.freemarker.core.BuiltInsForSequences.sort_byBI;
import org.apache.freemarker.core.BuiltInsForStringsMisc.evalBI;
+import org.apache.freemarker.core.BuiltInsForStringsMisc.evalJsonBI;
import org.apache.freemarker.core.model.TemplateCallableModel;
import org.apache.freemarker.core.model.TemplateDateModel;
import org.apache.freemarker.core.model.TemplateModelWithOriginName;
@@ -75,7 +76,7 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
protected ASTExpression target;
protected String key;
- static final int NUMBER_OF_BIS = 271;
+ static final int NUMBER_OF_BIS = 272;
static final HashMap<String, ASTExpBuiltIn> BUILT_INS_BY_NAME = new HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
static {
@@ -104,6 +105,7 @@ abstract class ASTExpBuiltIn extends ASTExpression implements Cloneable {
putBI("ensureStartsWith", new BuiltInsForStringsBasic.ensure_starts_withBI());
putBI("esc", new escBI());
putBI("eval", new evalBI());
+ putBI("evalJson", new evalJsonBI());
putBI("first", new firstBI());
putBI("float", new floatBI());
putBI("floor", new floorBI());
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
index d5bb4bf..3fcb4e2 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java
@@ -72,14 +72,14 @@ class BuiltInsForStringsMisc {
}
static class evalBI extends OutputFormatBoundBuiltIn {
-
+
private ParsingConfiguration pCfg;
-
+
@Override
protected TemplateModel calculateResult(Environment env) throws TemplateException {
return calculateResult(BuiltInForString.getTargetString(target, env), env);
}
-
+
@Override
void bindToOutputFormat(OutputFormat outputFormat, AutoEscapingPolicy autoEscapingPolicy) {
super.bindToOutputFormat(outputFormat, autoEscapingPolicy);
@@ -89,12 +89,12 @@ class BuiltInsForStringsMisc {
pCfg = new FinalParsingConfiguration(pCfg, pCfg.getTemplateLanguage(), outputFormat, autoEscapingPolicy,
template.getConfiguration());
}
- this.pCfg = pCfg;
+ this.pCfg = pCfg;
}
TemplateModel calculateResult(String s, Environment env) throws TemplateException {
Template parentTemplate = getTemplate();
-
+
ASTExpression exp;
try {
try {
@@ -112,7 +112,7 @@ class BuiltInsForStringsMisc {
parentTemplate, false, tkMan,
pCfg,
null);
-
+
exp = parser.Expression();
} catch (TokenMgrError e) {
throw e.toParseException(parentTemplate);
@@ -136,21 +136,37 @@ class BuiltInsForStringsMisc {
"\n\nThe failing expression:");
}
}
-
+
}
-
+
+ static class evalJsonBI extends BuiltInForString {
+ @Override
+ TemplateModel calculateResult(String s, Environment env) throws TemplateException {
+ try {
+ return JSONParser.parse(s);
+ } catch (JSONParser.JSONParseException e) {
+ throw new TemplateException(e, this, env,
+ "Failed to \"?", key, "\" string with this error:\n\n",
+ MessageUtils.EMBEDDED_MESSAGE_BEGIN,
+ new _DelayedGetMessage(e),
+ MessageUtils.EMBEDDED_MESSAGE_END,
+ "\n\nThe failing expression:");
+ }
+ }
+ }
+
/**
* A method that takes a parameter and evaluates it as a string,
* then treats that string as template source code and returns a
* transform model that evaluates the template in place.
* The template inherits the configuration and environment of the executing
- * template. By default, its name will be equal to
+ * template. By default, its name will be equal to
* <tt>executingTemplate.getLookupName() + "$anonymous_interpreted"</tt>. You can
* specify another parameter to the method call in which case the
* template name suffix is the specified id instead of "anonymous_interpreted".
*/
static class interpretBI extends OutputFormatBoundBuiltIn {
-
+
/**
* Constructs a template on-the-fly and returns it embedded in a
* {@link TemplateDirectiveModel}.
@@ -372,5 +388,5 @@ class BuiltInsForStringsMisc {
}
}
-
+
}
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/JSONParser.java b/freemarker-core/src/main/java/org/apache/freemarker/core/JSONParser.java
new file mode 100644
index 0000000..5c5ae48
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/JSONParser.java
@@ -0,0 +1,616 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.freemarker.core;
+
+import java.math.BigDecimal;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNullModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.model.TemplateStringModel;
+import org.apache.freemarker.core.model.impl.SimpleHash;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleString;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util._NumberUtils;
+import org.apache.freemarker.core.util._StringUtils;
+
+import jdk.nashorn.internal.objects.NativeNumber;
+
+/**
+ * JSON parser that returns a {@link TameplatModel}, similar to what FTL literals product (and so, what
+ * @code ?eval} would return). A notable difference compared to the result FTL literals is that this doesn't use the
+ * {@link ParserConfiguration#getArithmeticEngine()} to parse numbers, as JSON has its own fixed number syntax. For
+ * numbers this parser returns {@link SimpleNumberModel}-s, where the wrapped numbers will be {@link Integer}-s when
+ * they fit into that, otherwise they will be {@link Long}-s if they fit into that, otherwise they will be
+ * {@link BigDecimal}-s.
+ *
+ * <p>This parser allows certain things that are errors in pure JSON:
+ * <ul>
+ * <li>JavaScript comments are supported</li>
+ * <li>Non-breaking space (nbsp) and BOM are treated as whitespace</li>
+ * </ul>
+ */
+class JSONParser {
+
+ private static final String UNCLOSED_OBJECT_MESSAGE
+ = "This {...} was still unclosed when the end of the file was reached. (Look for a missing \"}\")";
+
+ private static final String UNCLOSED_ARRAY_MESSAGE
+ = "This [...] was still unclosed when the end of the file was reached. (Look for a missing \"]\")";
+
+ private static final BigDecimal MIN_INT_AS_BIGDECIMAL = BigDecimal.valueOf(Integer.MIN_VALUE);
+ private static final BigDecimal MAX_INT_AS_BIGDECIMAL = BigDecimal.valueOf(Integer.MAX_VALUE);
+ private static final BigDecimal MIN_LONG_AS_BIGDECIMAL = BigDecimal.valueOf(Long.MIN_VALUE);
+ private static final BigDecimal MAX_LONG_AS_BIGDECIMAL = BigDecimal.valueOf(Long.MAX_VALUE);
+
+ private final String src;
+ private final int ln;
+
+ private int p;
+
+ public static TemplateModel parse(String src) throws JSONParseException {
+ return new JSONParser(src).parse();
+ }
+
+ /**
+ * @param sourceLocation Only used in error messages, maybe {@code null}.
+ */
+ private JSONParser(String src) {
+ this.src = src;
+ this.ln = src.length();
+ }
+
+ private TemplateModel parse() throws JSONParseException {
+ skipWS();
+ TemplateModel result = consumeValue("Empty JSON (contains no value)", p);
+
+ skipWS();
+ if (p != ln) {
+ throw newParseException("End-of-file was expected but found further non-whitespace characters.");
+ }
+
+ return result;
+ }
+
+ private TemplateModel consumeValue(String eofErrorMessage, int eofBlamePosition) throws JSONParseException {
+ if (p == ln) {
+ throw newParseException(
+ eofErrorMessage == null
+ ? "A value was expected here, but end-of-file was reached." : eofErrorMessage,
+ eofBlamePosition == -1 ? p : eofBlamePosition);
+ }
+
+ TemplateModel result;
+
+ result = tryConsumeString();
+ if (result != null) return result;
+
+ result = tryConsumeNumber();
+ if (result != null) return result;
+
+ result = tryConsumeObject();
+ if (result != null) return result;
+
+ result = tryConsumeArray();
+ if (result != null) return result;
+
+ result = tryConsumeTrueFalseNull();
+ if (result != null) return result;
+
+ // Better error message for a frequent mistake:
+ if (p < ln && src.charAt(p) == '\'') {
+ throw newParseException("Unexpected apostrophe-quote character. "
+ + "JSON strings must be quoted with quotation mark.");
+ }
+
+ throw newParseException(
+ "Expected either the beginning of a (negative) number or the beginning of one of these: "
+ + "{...}, [...], \"...\", true, false, null. Found character "
+ + _StringUtils.jQuote(src.charAt(p)) + " instead.");
+ }
+
+ private TemplateModel tryConsumeTrueFalseNull() throws JSONParseException {
+ int startP = p;
+ if (p < ln && isIdentifierStart(src.charAt(p))) {
+ p++;
+ while (p < ln && isIdentifierPart(src.charAt(p))) {
+ p++;
+ }
+ }
+
+ if (startP == p) return null;
+
+ String keyword = src.substring(startP, p);
+ if (keyword.equals("true")) {
+ return TemplateBooleanModel.TRUE;
+ } else if (keyword.equals("false")) {
+ return TemplateBooleanModel.FALSE;
+ } else if (keyword.equals("null")) {
+ return TemplateNullModel.INSTANCE;
+ }
+
+ throw newParseException(
+ "Invalid JSON keyword: " + _StringUtils.jQuote(keyword)
+ + ". Should be one of: true, false, null. "
+ + "If it meant to be a string then it must be quoted.", startP);
+ }
+
+ private TemplateNumberModel tryConsumeNumber() throws JSONParseException {
+ if (p >= ln) {
+ return null;
+ }
+ char c = src.charAt(p);
+ boolean negative = c == '-';
+ if (!(negative || isDigit(c) || c == '.')) {
+ return null;
+ }
+
+ int startP = p;
+
+ if (negative) {
+ if (p + 1 >= ln) {
+ throw newParseException("Expected a digit after \"-\", but reached end-of-file.");
+ }
+ char lookAheadC = src.charAt(p + 1);
+ if (!(isDigit(lookAheadC) || lookAheadC == '.')) {
+ return null;
+ }
+ p++; // Consume "-" only, not the digit
+ }
+
+ long longSum = 0;
+ boolean firstDigit = true;
+ consumeLongFittingHead: do {
+ c = src.charAt(p);
+
+ if (!isDigit(c)) {
+ if (c == '.' && firstDigit) {
+ throw newParseException("JSON doesn't allow numbers starting with \".\".");
+ }
+ break consumeLongFittingHead;
+ }
+
+ int digit = c - '0';
+ if (longSum == 0) {
+ if (!firstDigit) {
+ throw newParseException("JSON doesn't allow superfluous leading 0-s.", p - 1);
+ }
+
+ longSum = !negative ? digit : -digit;
+ p++;
+ } else {
+ long prevLongSum = longSum;
+ longSum = longSum * 10 + (!negative ? digit : -digit);
+ if (!negative && prevLongSum > longSum || negative && prevLongSum < longSum) {
+ // We had an overflow => Can't consume this digit as long-fitting
+ break consumeLongFittingHead;
+ }
+ p++;
+ }
+ firstDigit = false;
+ } while (p < ln);
+
+ if (p < ln && isBigDecimalFittingTailCharacter(c)) {
+ char lastC = c;
+ p++;
+
+ consumeBigDecimalFittingTail: while (p < ln) {
+ c = src.charAt(p);
+ if (isBigDecimalFittingTailCharacter(c)) {
+ p++;
+ } else if ((c == '+' || c == '-') && isE(lastC)) {
+ p++;
+ } else {
+ break consumeBigDecimalFittingTail;
+ }
+ lastC = c;
+ }
+
+ String numStr = src.substring(startP, p);
+ BigDecimal bd;
+ try {
+ bd = new BigDecimal(numStr);
+ } catch (NumberFormatException e) {
+ throw new JSONParseException("Malformed number: " + numStr, src, startP, e);
+ }
+
+ if (bd.compareTo(MIN_INT_AS_BIGDECIMAL) >= 0 && bd.compareTo(MAX_INT_AS_BIGDECIMAL) <= 0) {
+ if (_NumberUtils.isIntegerBigDecimal(bd)) {
+ return new SimpleNumber(bd.intValue());
+ }
+ } else if (bd.compareTo(MIN_LONG_AS_BIGDECIMAL) >= 0 && bd.compareTo(MAX_LONG_AS_BIGDECIMAL) <= 0) {
+ if (_NumberUtils.isIntegerBigDecimal(bd)) {
+ return new SimpleNumber(bd.longValue());
+ }
+ }
+ return new SimpleNumber(bd);
+ } else {
+ return new SimpleNumber(
+ longSum <= Integer.MAX_VALUE && longSum >= Integer.MIN_VALUE
+ ? (Number) (int) longSum
+ : longSum);
+ }
+ }
+
+ private TemplateStringModel tryConsumeString() throws JSONParseException {
+ int startP = p;
+ if (!tryConsumeChar('"')) return null;
+
+ StringBuilder sb = new StringBuilder();
+ char c = 0;
+ while (p < ln) {
+ c = src.charAt(p);
+
+ if (c == '"') {
+ p++;
+ return new SimpleString(sb.toString()); // Call normally returns here!
+ } else if (c == '\\') {
+ p++;
+ sb.append(consumeAfterBackslash());
+ } else if (c <= 0x1F) {
+ throw newParseException("JSON doesn't allow unescaped control characters in string literals, "
+ + "but found character with code (decimal): " + (int) c);
+ } else {
+ p++;
+ sb.append(c);
+ }
+ }
+
+ throw newParseException("String literal was still unclosed when the end of the file was reached. "
+ + "(Look for missing or accidentally escaped closing quotation mark.)", startP);
+ }
+
+ private TemplateSequenceModel tryConsumeArray() throws JSONParseException {
+ int startP = p;
+ if (!tryConsumeChar('[')) return null;
+
+ skipWS();
+ if (tryConsumeChar(']')) return TemplateSequenceModel.EMPTY_SEQUENCE;
+
+ boolean afterComma = false;
+ NativeSequence elements = new NativeSequence();
+ do {
+ skipWS();
+ elements.add(consumeValue(afterComma ? null : UNCLOSED_ARRAY_MESSAGE, afterComma ? -1 : startP));
+
+ skipWS();
+ afterComma = true;
+ } while (consumeChar(',', ']', UNCLOSED_ARRAY_MESSAGE, startP) == ',');
+ return elements;
+ }
+
+ private TemplateHashModelEx tryConsumeObject() throws JSONParseException {
+ int startP = p;
+ if (!tryConsumeChar('{')) return null;
+
+ skipWS();
+ if (tryConsumeChar('}')) return TemplateHashModelEx.EMPTY_HASH;
+
+ boolean afterComma = false;
+ NativeHashEx hash = new NativeHashEx();
+ do {
+ skipWS();
+ int keyStartP = p;
+ Object key = consumeValue(afterComma ? null : UNCLOSED_OBJECT_MESSAGE, afterComma ? -1 : startP);
+ if (!(key instanceof TemplateStringModel)) {
+ throw newParseException("Wrong key type. JSON only allows string keys inside {...}.", keyStartP);
+ }
+ String strKey = null;
+ try {
+ strKey = ((TemplateStringModel) key).getAsString();
+ } catch (TemplateException e) {
+ throw new BugException(e);
+ }
+
+ skipWS();
+ consumeChar(':');
+
+ skipWS();
+ hash.put(strKey, consumeValue(null, -1));
+
+ skipWS();
+ afterComma = true;
+ } while (consumeChar(',', '}', UNCLOSED_OBJECT_MESSAGE, startP) == ',');
+ return hash;
+ }
+
+ private boolean isE(char c) {
+ return c == 'e' || c == 'E';
+ }
+
+ private boolean isBigDecimalFittingTailCharacter(char c) {
+ return c == '.' || isE(c) || isDigit(c);
+ }
+
+ private char consumeAfterBackslash() throws JSONParseException {
+ if (p == ln) {
+ throw newParseException("Reached the end of the file, but the escape is unclosed.");
+ }
+
+ final char c = src.charAt(p);
+ switch (c) {
+ case '"':
+ case '\\':
+ case '/':
+ p++;
+ return c;
+ case 'b':
+ p++;
+ return '\b';
+ case 'f':
+ p++;
+ return '\f';
+ case 'n':
+ p++;
+ return '\n';
+ case 'r':
+ p++;
+ return '\r';
+ case 't':
+ p++;
+ return '\t';
+ case 'u':
+ p++;
+ return consumeAfterBackslashU();
+ }
+ throw newParseException("Unsupported escape: \\" + c);
+ }
+
+ private char consumeAfterBackslashU() throws JSONParseException {
+ if (p + 3 >= ln) {
+ throw newParseException("\\u must be followed by exactly 4 hexadecimal digits");
+ }
+ final String hex = src.substring(p, p + 4);
+ try {
+ char r = (char) Integer.parseInt(hex, 16);
+ p += 4;
+ return r;
+ } catch (NumberFormatException e) {
+ throw newParseException("\\u must be followed by exactly 4 hexadecimal digits, but was followed by "
+ + _StringUtils.jQuote(hex) + ".");
+ }
+ }
+
+ private boolean tryConsumeChar(char c) {
+ if (p < ln && src.charAt(p) == c) {
+ p++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void consumeChar(char expected) throws JSONParseException {
+ consumeChar(expected, (char) 0, null, -1);
+ }
+
+ private char consumeChar(char expected1, char expected2, String eofErrorHint, int eofErrorP) throws JSONParseException {
+ if (p >= ln) {
+ throw newParseException(eofErrorHint == null
+ ? "Expected " + _StringUtils.jQuote(expected1)
+ + ( expected2 != 0 ? " or " + _StringUtils.jQuote(expected2) : "")
+ + " character, but reached end-of-file. "
+ : eofErrorHint,
+ eofErrorP == -1 ? p : eofErrorP);
+ }
+ char c = src.charAt(p);
+ if (c == expected1 || (expected2 != 0 && c == expected2)) {
+ p++;
+ return c;
+ }
+ throw newParseException("Expected " + _StringUtils.jQuote(expected1)
+ + ( expected2 != 0 ? " or " + _StringUtils.jQuote(expected2) : "")
+ + " character, but found " + _StringUtils.jQuote(c) + " instead.");
+ }
+
+ private void skipWS() throws JSONParseException {
+ do {
+ while (p < ln && isWS(src.charAt(p))) {
+ p++;
+ }
+ } while (skipComment());
+ }
+
+ private boolean skipComment() throws JSONParseException {
+ if (p + 1 < ln) {
+ if (src.charAt(p) == '/') {
+ char c2 = src.charAt(p + 1);
+ if (c2 == '/') {
+ int eolP = p + 2;
+ while (eolP < ln && !isLineBreak(src.charAt(eolP))) {
+ eolP++;
+ }
+ p = eolP;
+ return true;
+ } else if (c2 == '*') {
+ int closerP = p + 3;
+ while (closerP < ln && !(src.charAt(closerP - 1) == '*' && src.charAt(closerP) == '/')) {
+ closerP++;
+ }
+ if (closerP >= ln) {
+ throw newParseException("Unclosed comment");
+ }
+ p = closerP + 1;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Whitespace as specified by JSON, plus non-breaking space (nbsp), and BOM.
+ */
+ private static boolean isWS(char c) {
+ return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 0xA0 || c == '\uFEFF';
+ }
+
+ private static boolean isLineBreak(char c) {
+ return c == '\r' || c == '\n';
+ }
+
+ private static boolean isIdentifierStart(char c) {
+ return Character.isLetter(c) || c == '_' || c == '$';
+ }
+
+ private static boolean isDigit(char c) {
+ return c >= '0' && c <= '9';
+ }
+
+ private static boolean isIdentifierPart(char c) {
+ return isIdentifierStart(c) || isDigit(c);
+ }
+
+ private JSONParseException newParseException(String message) {
+ return newParseException(message, p);
+ }
+
+ private JSONParseException newParseException(String message, int p) {
+ return new JSONParseException(message, src, p);
+ }
+
+ static class JSONParseException extends Exception {
+ public JSONParseException(String message, String src, int position) {
+ super(createSourceCodeErrorMessage(message, src, position));
+ }
+
+ public JSONParseException(String message, String src, int position,
+ Throwable cause) {
+ super(createSourceCodeErrorMessage(message, src, position), cause);
+ }
+
+ }
+
+ private static int MAX_QUOTATION_LENGTH = 50;
+
+ private static String createSourceCodeErrorMessage(String message, String srcCode, int position) {
+ int ln = srcCode.length();
+ if (position < 0) {
+ position = 0;
+ }
+ if (position >= ln) {
+ return message + "\n"
+ + "Error location: At the end of text.";
+ }
+
+ int i;
+ char c;
+ int rowBegin = 0;
+ int rowEnd;
+ int row = 1;
+ char lastChar = 0;
+ for (i = 0; i <= position; i++) {
+ c = srcCode.charAt(i);
+ if (lastChar == 0xA) {
+ rowBegin = i;
+ row++;
+ } else if (lastChar == 0xD && c != 0xA) {
+ rowBegin = i;
+ row++;
+ }
+ lastChar = c;
+ }
+ for (i = position; i < ln; i++) {
+ c = srcCode.charAt(i);
+ if (c == 0xA || c == 0xD) {
+ if (c == 0xA && i > 0 && srcCode.charAt(i - 1) == 0xD) {
+ i--;
+ }
+ break;
+ }
+ }
+ rowEnd = i - 1;
+ if (position > rowEnd + 1) {
+ position = rowEnd + 1;
+ }
+ int col = position - rowBegin + 1;
+ if (rowBegin > rowEnd) {
+ return message + "\n"
+ + "Error location: line "
+ + row + ", column " + col + ":\n"
+ + "(Can't show the line because it is empty.)";
+ }
+ String s1 = srcCode.substring(rowBegin, position);
+ String s2 = srcCode.substring(position, rowEnd + 1);
+ s1 = expandTabs(s1, 8);
+ int ln1 = s1.length();
+ s2 = expandTabs(s2, 8, ln1);
+ int ln2 = s2.length();
+ if (ln1 + ln2 > MAX_QUOTATION_LENGTH) {
+ int newLn2 = ln2 - ((ln1 + ln2) - MAX_QUOTATION_LENGTH);
+ if (newLn2 < 6) {
+ newLn2 = 6;
+ }
+ if (newLn2 < ln2) {
+ s2 = s2.substring(0, newLn2 - 3) + "...";
+ ln2 = newLn2;
+ }
+ if (ln1 + ln2 > MAX_QUOTATION_LENGTH) {
+ s1 = "..." + s1.substring((ln1 + ln2) - MAX_QUOTATION_LENGTH + 3);
+ }
+ }
+ StringBuilder res = new StringBuilder(message.length() + 80);
+ res.append(message);
+ res.append("\nError location: line ").append(row).append(", column ").append(col).append(":\n");
+ res.append(s1).append(s2).append("\n");
+ int x = s1.length();
+ while (x != 0) {
+ res.append(' ');
+ x--;
+ }
+ res.append('^');
+
+ return res.toString();
+ }
+
+ private static String expandTabs(String s, int tabWidth) {
+ return expandTabs(s, tabWidth, 0);
+ }
+
+ /**
+ * Replaces all tab-s with spaces in a single line.
+ */
+ private static String expandTabs(String s, int tabWidth, int startCol) {
+ int e = s.indexOf('\t');
+ if (e == -1) {
+ return s;
+ }
+ int b = 0;
+ StringBuilder buf = new StringBuilder(s.length() + Math.max(16, tabWidth * 2));
+ do {
+ buf.append(s, b, e);
+ int col = buf.length() + startCol;
+ for (int i = tabWidth * (1 + col / tabWidth) - col; i > 0; i--) {
+ buf.append(' ');
+ }
+ b = e + 1;
+ e = s.indexOf('\t', b);
+ } while (e != -1);
+ buf.append(s, b, s.length());
+ return buf.toString();
+ }
+
+}