You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2018/02/28 11:37:52 UTC
[3/7] groovy git commit: GROOVY-8379: Rework groovy-json
FastStringUtils (closes #667)
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonFastParser.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonFastParser.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonFastParser.java
new file mode 100644
index 0000000..4cc219c
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonFastParser.java
@@ -0,0 +1,334 @@
+/*
+ * 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.groovy.json.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This works in index overlay mode or chop mode.
+ * Chop mode reduces possibility of memory leak but causes a few more buffer copies as it chops up the buffer.
+ *
+ * @author Rick Hightower
+ */
+public class JsonFastParser extends JsonParserCharArray {
+
+ private final boolean useValues;
+ private final boolean chop;
+ private final boolean lazyChop;
+ private final boolean checkDates;
+
+ public JsonFastParser() {
+ this(true);
+ }
+
+ public JsonFastParser(boolean useValues) {
+ this(useValues, false);
+ }
+
+ public JsonFastParser(boolean useValues, boolean chop) {
+ this(useValues, chop, !chop);
+ }
+
+ public JsonFastParser(boolean useValues, boolean chop, boolean lazyChop) {
+ this(useValues, chop, lazyChop, true);
+ }
+
+ public JsonFastParser(boolean useValues, boolean chop, boolean lazyChop, boolean checkDates) {
+ this.useValues = useValues;
+ this.chop = chop;
+ this.lazyChop = lazyChop;
+ this.checkDates = checkDates;
+ }
+
+ protected final Value decodeJsonObjectLazyFinalParse() {
+ char[] array = charArray;
+
+ if (__currentChar == '{')
+ __index++;
+
+ ValueMap map = useValues ? new ValueMapImpl() : new LazyValueMap(lazyChop);
+ Value value = new ValueContainer(map);
+
+ objectLoop:
+ for (; __index < array.length; __index++) {
+ skipWhiteSpace();
+ switch (__currentChar) {
+
+ case '"':
+ Value key = decodeStringOverlay();
+ skipWhiteSpace();
+
+ if (__currentChar != ':') {
+
+ complain("expecting current character to be " + charDescription(__currentChar) + "\n");
+ }
+ __index++;
+
+ Value item = decodeValueOverlay();
+
+ skipWhiteSpace();
+
+ MapItemValue miv = new MapItemValue(key, item);
+
+ map.add(miv);
+ }
+
+ switch (__currentChar) {
+ case '}':
+ __index++;
+ break objectLoop;
+
+ case ',':
+ continue;
+
+ default:
+
+ complain(
+ "expecting '}' or ',' but got current char " + charDescription(__currentChar));
+ }
+ }
+ return value;
+ }
+
+ protected Value decodeValue() {
+ return decodeValueOverlay();
+ }
+
+ private Value decodeValueOverlay() {
+ skipWhiteSpace();
+
+ switch (__currentChar) {
+ case '"':
+ return decodeStringOverlay();
+
+ case '{':
+ return decodeJsonObjectLazyFinalParse();
+
+ case 't':
+ return decodeTrue() ? ValueContainer.TRUE : ValueContainer.FALSE;
+
+ case 'f':
+ return !decodeFalse() ? ValueContainer.FALSE : ValueContainer.TRUE;
+
+ case 'n':
+ return decodeNull() == null ? ValueContainer.NULL : ValueContainer.NULL;
+
+ case '[':
+ return decodeJsonArrayOverlay();
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '0':
+ return decodeNumberOverlay(false);
+
+ case '-':
+ return decodeNumberOverlay(true);
+
+ default:
+ complain("Unable to determine the " +
+ "current character, it is not a string, number, array, or object");
+ return null;
+ }
+ }
+
+ private Value decodeNumberOverlay(final boolean minus) {
+ char[] array = charArray;
+
+ final int startIndex = __index;
+ int index = __index;
+ char currentChar;
+ boolean doubleFloat = false;
+ boolean foundDot = false;
+ boolean foundSign = false;
+ boolean foundExp = false;
+
+ if (minus && index + 1 < array.length) {
+ index++;
+ }
+
+ while (true) {
+ currentChar = array[index];
+ if (isNumberDigit(currentChar)) {
+ //noop
+ } else if (currentChar <= 32) { //white
+ break;
+ } else if (isDelimiter(currentChar)) {
+ break;
+ } else if (isDecimalChar(currentChar)) {
+ switch (currentChar) {
+ case DECIMAL_POINT:
+ if (foundDot || foundExp) { complain("unexpected character " + currentChar); }
+ foundDot = true;
+ break;
+ case LETTER_E:
+ case LETTER_BIG_E:
+ if (foundExp) { complain("unexpected character " + currentChar); }
+ foundExp = true;
+ break;
+ case MINUS:
+ case PLUS:
+ if (foundSign || !foundExp) { complain("unexpected character " + currentChar); }
+ if (foundExp && array[index - 1] != LETTER_E && array[index - 1] != LETTER_BIG_E) {
+ complain("unexpected character " + currentChar);
+ }
+ foundSign = true;
+ break;
+ }
+ doubleFloat = true;
+ } else {
+ complain("unexpected character " + currentChar);
+ }
+ index++;
+ if (index >= array.length) break;
+ }
+
+ // Handle the case where the exponential number ends without the actual exponent
+ if (foundExp) {
+ char prevChar = array[index - 1];
+ if (prevChar == LETTER_E || prevChar == LETTER_BIG_E || prevChar == MINUS || prevChar == PLUS) {
+ complain("unexpected character " + currentChar);
+ }
+ }
+
+ __index = index;
+ __currentChar = currentChar;
+
+ Type type = doubleFloat ? Type.DOUBLE : Type.INTEGER;
+
+ return new NumberValue(chop, type, startIndex, __index, this.charArray);
+ }
+
+ private Value decodeStringOverlay() {
+ char[] array = charArray;
+ int index = __index;
+ char currentChar = charArray[index];
+
+ if (index < array.length && currentChar == '"') {
+ index++;
+ }
+
+ final int startIndex = index;
+
+ boolean encoded = hasEscapeChar(array, index, indexHolder);
+ index = indexHolder[0];
+
+ if (encoded) {
+ index = findEndQuote(array, index);
+ }
+
+ Value value = new CharSequenceValue(chop, Type.STRING, startIndex, index, array, encoded, checkDates);
+
+ if (index < array.length) {
+ index++;
+ }
+
+ __index = index;
+ return value;
+ }
+
+ private Value decodeJsonArrayOverlay() {
+ char[] array = charArray;
+ if (__currentChar == '[') {
+ __index++;
+ }
+
+ skipWhiteSpace();
+
+ /* the list might be empty */
+ if (__currentChar == ']') {
+ __index++;
+ return new ValueContainer(new ArrayList());
+ }
+
+ List<Object> list;
+
+ if (useValues) {
+ list = new ArrayList<Object>();
+ } else {
+ list = new ValueList(lazyChop);
+ }
+
+ Value value = new ValueContainer(list);
+
+ Value item;
+ char c;
+ int lastIndex;
+ boolean foundEnd = false;
+
+ arrayLoop:
+ for (; __index < array.length; __index++) {
+ item = decodeValueOverlay();
+
+ list.add(item);
+ c = currentChar();
+ switch (c) {
+ case ',':
+ continue;
+ case ']':
+ __index++;
+ foundEnd = true;
+ break arrayLoop;
+ }
+
+ lastIndex = __index;
+ skipWhiteSpace();
+ c = currentChar();
+
+ switch (c) {
+ case ',':
+ continue;
+ case ']':
+ if (__index == lastIndex) {
+ complain("missing ]");
+ }
+ foundEnd = true;
+ __index++;
+ break arrayLoop;
+ default:
+ complain(
+ String.format("expecting a ',' or a ']', " +
+ " but got \nthe current character of %s " +
+ " on array size of %s \n", charDescription(__currentChar), list.size())
+ );
+ }
+ }
+
+ if (!foundEnd) {
+ complain("Did not find end of Json Array");
+ }
+ return value;
+ }
+
+ protected final Object decodeFromChars(char[] cs) {
+ Value value = ((Value) super.decodeFromChars(cs));
+ if (value.isContainer()) {
+ return value.toValue();
+ } else {
+ return value;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserCharArray.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserCharArray.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserCharArray.java
new file mode 100644
index 0000000..6fb6f4c
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserCharArray.java
@@ -0,0 +1,386 @@
+/*
+ * 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.groovy.json.internal;
+
+import groovy.json.JsonException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts an input JSON String into Java objects works with String or char array
+ * as input. Produces an Object which can be any of the basic JSON types mapped
+ * to Java.
+ * <p/>
+ *
+ * @author Rick Hightower
+ */
+public class JsonParserCharArray extends BaseJsonParser {
+
+ protected char[] charArray;
+ protected int __index;
+ protected char __currentChar;
+
+ private int lastIndex;
+
+ protected Object decodeFromChars(char[] cs) {
+ __index = 0;
+ charArray = cs;
+ lastIndex = cs.length - 1;
+ return decodeValue();
+ }
+
+ protected final boolean hasMore() {
+ return __index < lastIndex;
+ }
+
+ protected final boolean hasCurrent() {
+ return __index <= lastIndex;
+ }
+
+ protected final void skipWhiteSpace() {
+ int ix = __index;
+
+ if (hasCurrent()) {
+ this.__currentChar = this.charArray[ix];
+ }
+
+ if (__currentChar <= 32) {
+ ix = skipWhiteSpaceFast(this.charArray, ix);
+ this.__currentChar = this.charArray[ix];
+ __index = ix;
+ }
+ }
+
+ protected final char nextChar() {
+ try {
+ if (hasMore()) {
+ __index++;
+ return __currentChar = charArray[__index];
+ } else {
+ // TODO move unicode 0 to separate file to avoid doc parsing issues
+ return '\u0000';
+ }
+ } catch (Exception ex) {
+ throw new JsonException(exceptionDetails("unable to advance character"), ex);
+ }
+ }
+
+ protected String exceptionDetails(String message) {
+ return CharScanner.errorDetails(message, charArray, __index, __currentChar);
+ }
+
+ private static int skipWhiteSpaceFast(char[] array, int index) {
+ char c;
+ for (; index < array.length; index++) {
+ c = array[index];
+ if (c > 32) {
+ return index;
+ }
+ }
+ return index - 1;
+ }
+
+ protected final Object decodeJsonObject() {
+ if (__currentChar == '{') {
+ __index++;
+ }
+
+ LazyMap map = new LazyMap();
+
+ for (; __index < this.charArray.length; __index++) {
+ skipWhiteSpace();
+
+ if (__currentChar == '"') {
+ String key = decodeString();
+
+ if (internKeys) {
+ String keyPrime = internedKeysCache.get(key);
+ if (keyPrime == null) {
+ key = key.intern();
+ internedKeysCache.put(key, key);
+ } else {
+ key = keyPrime;
+ }
+ }
+
+ skipWhiteSpace();
+
+ if (__currentChar != ':') {
+ complain("expecting current character to be " + charDescription(__currentChar) + "\n");
+ }
+ __index++;
+
+ skipWhiteSpace();
+
+ Object value = decodeValueInternal();
+
+ skipWhiteSpace();
+ map.put(key, value);
+ }
+
+ if (__currentChar == '}') {
+ __index++;
+ break;
+ } else if (__currentChar == ',') {
+ continue;
+ } else {
+ complain(
+ "expecting '}' or ',' but got current char " + charDescription(__currentChar));
+ }
+ }
+
+ return map;
+ }
+
+ protected final void complain(String complaint) {
+ throw new JsonException(exceptionDetails(complaint));
+ }
+
+ protected Object decodeValue() {
+ return decodeValueInternal();
+ }
+
+ private Object decodeValueInternal() {
+ Object value = null;
+ skipWhiteSpace();
+
+ switch (__currentChar) {
+ case '"':
+ value = decodeString();
+ break;
+
+ case 't':
+ value = decodeTrue();
+ break;
+
+ case 'f':
+ value = decodeFalse();
+ break;
+
+ case 'n':
+ value = decodeNull();
+ break;
+
+ case '[':
+ value = decodeJsonArray();
+ break;
+
+ case '{':
+ value = decodeJsonObject();
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ value = decodeNumber();
+ break;
+ case '-':
+ value = decodeNumber();
+ break;
+
+ default:
+ throw new JsonException(exceptionDetails("Unable to determine the " +
+ "current character, it is not a string, number, array, or object"));
+ }
+
+ return value;
+ }
+
+ int[] endIndex = new int[1];
+
+ private Object decodeNumber() {
+ Number num = CharScanner.parseJsonNumber(charArray, __index, charArray.length, endIndex);
+ __index = endIndex[0];
+
+ return num;
+ }
+
+ protected static final char[] NULL = Chr.chars("null");
+
+ protected final Object decodeNull() {
+ if (__index + NULL.length <= charArray.length) {
+ if (charArray[__index] == 'n' &&
+ charArray[++__index] == 'u' &&
+ charArray[++__index] == 'l' &&
+ charArray[++__index] == 'l') {
+ __index++;
+ return null;
+ }
+ }
+ throw new JsonException(exceptionDetails("null not parse properly"));
+ }
+
+ protected static final char[] TRUE = Chr.chars("true");
+
+ protected final boolean decodeTrue() {
+ if (__index + TRUE.length <= charArray.length) {
+ if (charArray[__index] == 't' &&
+ charArray[++__index] == 'r' &&
+ charArray[++__index] == 'u' &&
+ charArray[++__index] == 'e') {
+
+ __index++;
+ return true;
+ }
+ }
+
+ throw new JsonException(exceptionDetails("true not parsed properly"));
+ }
+
+ protected static char[] FALSE = Chr.chars("false");
+
+ protected final boolean decodeFalse() {
+ if (__index + FALSE.length <= charArray.length) {
+ if (charArray[__index] == 'f' &&
+ charArray[++__index] == 'a' &&
+ charArray[++__index] == 'l' &&
+ charArray[++__index] == 's' &&
+ charArray[++__index] == 'e') {
+ __index++;
+ return false;
+ }
+ }
+ throw new JsonException(exceptionDetails("false not parsed properly"));
+ }
+
+ private CharBuf builder = CharBuf.create(20);
+
+ private String decodeString() {
+ char[] array = charArray;
+ int index = __index;
+ char currentChar = array[index];
+
+ if (index < array.length && currentChar == '"') {
+ index++;
+ }
+
+ final int startIndex = index;
+
+ boolean encoded = hasEscapeChar(array, index, indexHolder);
+ index = indexHolder[0];
+
+ String value = null;
+ if (encoded) {
+ index = findEndQuote(array, index);
+ value = builder.decodeJsonString(array, startIndex, index).toString();
+ builder.recycle();
+ } else {
+ value = new String(array, startIndex, (index - startIndex));
+ }
+
+ if (index < charArray.length) {
+ index++;
+ }
+ __index = index;
+ return value;
+ }
+
+ protected final List decodeJsonArray() {
+ ArrayList<Object> list = null;
+
+ boolean foundEnd = false;
+ char[] charArray = this.charArray;
+
+ try {
+ if (__currentChar == '[') {
+ __index++;
+ }
+
+ int lastIndex;
+
+ skipWhiteSpace();
+
+ /* the list might be empty */
+ if (__currentChar == ']') {
+ __index++;
+ return new ArrayList();
+ }
+
+ list = new ArrayList();
+
+ while (this.hasMore()) {
+ Object arrayItem = decodeValueInternal();
+
+ list.add(arrayItem);
+
+ char c = charArray[__index];
+
+ if (c == ',') {
+ __index++;
+ continue;
+ } else if (c == ']') {
+ __index++;
+ foundEnd = true;
+ break;
+ }
+
+ lastIndex = __index;
+ skipWhiteSpace();
+
+ c = charArray[__index];
+
+ if (c == ',') {
+ __index++;
+ continue;
+ } else if (c == ']' && lastIndex != __index) {
+ __index++;
+ foundEnd = true;
+ break;
+ } else {
+ String charString = charDescription(c);
+
+ complain(
+ String.format("expecting a ',' or a ']', " +
+ " but got \nthe current character of %s " +
+ " on array index of %s \n", charString, list.size())
+ );
+ }
+ }
+ } catch (Exception ex) {
+ if (ex instanceof JsonException) {
+ throw (JsonException) ex;
+ }
+ throw new JsonException(exceptionDetails("issue parsing JSON array"), ex);
+ }
+ if (!foundEnd) {
+ complain("Did not find end of Json Array");
+ }
+ return list;
+ }
+
+ protected final char currentChar() {
+ if (__index > lastIndex) {
+ return 0;
+ } else {
+ return charArray[__index];
+ }
+ }
+
+ public Object parse(char[] chars) {
+ return this.decodeFromChars(chars);
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserLax.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserLax.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserLax.java
new file mode 100644
index 0000000..89e1fa4
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserLax.java
@@ -0,0 +1,677 @@
+/*
+ * 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.groovy.json.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Richard Hightower
+ */
+public class JsonParserLax extends JsonParserCharArray {
+
+ private final boolean useValues;
+ private final boolean chop;
+ private final boolean lazyChop;
+ private final boolean defaultCheckDates;
+
+ public JsonParserLax() {
+ this(true);
+ }
+
+ public JsonParserLax(boolean useValues) {
+ this(useValues, false);
+ }
+
+ public JsonParserLax(boolean useValues, boolean chop) {
+ this(useValues, chop, !chop);
+ }
+
+ public JsonParserLax(boolean useValues, boolean chop, boolean lazyChop) {
+ this(useValues, chop, lazyChop, true);
+ }
+
+ public JsonParserLax(boolean useValues, boolean chop, boolean lazyChop, boolean defaultCheckDates) {
+ this.useValues = useValues;
+ this.chop = chop;
+ this.lazyChop = lazyChop;
+ this.defaultCheckDates = defaultCheckDates;
+ }
+
+ private Value decodeJsonObjectLax() {
+ if (__currentChar == '{')
+ this.nextChar();
+
+ ValueMap map = useValues ? new ValueMapImpl() : new LazyValueMap(lazyChop);
+ Value value = new ValueContainer(map);
+
+ skipWhiteSpace();
+ int startIndexOfKey = __index;
+ Value key;
+ MapItemValue miv;
+ Value item;
+
+ done:
+ for (; __index < this.charArray.length; __index++) {
+ skipWhiteSpace();
+
+ switch (__currentChar) {
+ case ':':
+ char startChar = charArray[startIndexOfKey];
+ if (startChar == ',') {
+ startIndexOfKey++;
+ }
+
+ key = extractLaxString(startIndexOfKey, __index - 1, false, false);
+ __index++; //skip :
+
+ item = decodeValueInternal();
+ skipWhiteSpace();
+
+ miv = new MapItemValue(key, item);
+
+ map.add(miv);
+
+ startIndexOfKey = __index;
+ if (__currentChar == '}') {
+ __index++;
+ break done;
+ }
+
+ break;
+
+ case '\'':
+ key = decodeStringSingle();
+
+ //puts ("key with quote", key);
+
+ skipWhiteSpace();
+
+ if (__currentChar != ':') {
+ complain("expecting current character to be ':' but got " + charDescription(__currentChar) + "\n");
+ }
+ __index++;
+ item = decodeValueInternal();
+
+ //puts ("key", "#" + key + "#", value);
+
+ skipWhiteSpace();
+
+ miv = new MapItemValue(key, item);
+
+ map.add(miv);
+ startIndexOfKey = __index;
+ if (__currentChar == '}') {
+ __index++;
+ break done;
+ }
+
+ break;
+
+ case '"':
+ key = decodeStringDouble();
+
+ //puts ("key with quote", key);
+
+ skipWhiteSpace();
+
+ if (__currentChar != ':') {
+ complain("expecting current character to be ':' but got " + charDescription(__currentChar) + "\n");
+ }
+ __index++;
+ item = decodeValueInternal();
+
+ //puts ("key", "#" + key + "#", value);
+
+ skipWhiteSpace();
+
+ miv = new MapItemValue(key, item);
+
+ map.add(miv);
+ startIndexOfKey = __index;
+ if (__currentChar == '}') {
+ __index++;
+ break done;
+ }
+
+ break;
+ }
+
+ switch (__currentChar) {
+ case '}':
+ __index++;
+ break done;
+
+ case '/': /* */ //
+ handleComment();
+ startIndexOfKey = __index;
+ break;
+
+ case '#':
+ handleBashComment();
+ startIndexOfKey = __index;
+ break;
+ }
+ }
+
+ return value;
+ }
+
+ private Value extractLaxString(int startIndexOfKey, int end, boolean encoded, boolean checkDate) {
+ char startChar;
+ startIndexLookup:
+ for (; startIndexOfKey < __index && startIndexOfKey < charArray.length; startIndexOfKey++) {
+ startChar = charArray[startIndexOfKey];
+ switch (startChar) {
+ case ' ':
+ case '\n':
+ case '\t':
+ continue;
+
+ default:
+ break startIndexLookup;
+ }
+ }
+
+ char endChar;
+ int endIndex = end >= charArray.length ? charArray.length - 1 : end;
+ endIndexLookup:
+ for (; endIndex >= startIndexOfKey + 1 && endIndex >= 0; endIndex--) {
+ endChar = charArray[endIndex];
+ switch (endChar) {
+ case ' ':
+ case '\n':
+ case '\t':
+ case '}':
+ continue;
+ case ',':
+ case ';':
+ continue;
+
+ case ']':
+ continue;
+ default:
+ break endIndexLookup;
+ }
+ }
+ return new CharSequenceValue(chop, Type.STRING, startIndexOfKey, endIndex + 1, this.charArray, encoded, checkDate);
+ }
+
+ protected final Object decodeValue() {
+ return this.decodeValueInternal();
+ }
+
+ private Value decodeValueInternal() {
+ Value value = null;
+
+ for (; __index < charArray.length; __index++) {
+ skipWhiteSpace();
+
+ switch (__currentChar) {
+ case '\n':
+ break;
+
+ case '\r':
+ break;
+
+ case ' ':
+ break;
+
+ case '\t':
+ break;
+
+ case '\b':
+ break;
+
+ case '\f':
+ break;
+
+ case '/': /* */ //
+ handleComment();
+ break;
+
+ case '#':
+ handleBashComment();
+ break;
+
+ case '"':
+ value = decodeStringDouble();
+ break;
+
+ case '\'':
+ value = decodeStringSingle();
+ break;
+
+ case 't':
+ if (isTrue()) {
+ return decodeTrue() ? ValueContainer.TRUE : ValueContainer.FALSE;
+ } else {
+ value = decodeStringLax();
+ }
+ break;
+
+ case 'f':
+ if (isFalse()) {
+ return !decodeFalse() ? ValueContainer.FALSE : ValueContainer.TRUE;
+ } else {
+ value = decodeStringLax();
+ }
+ break;
+
+ case 'n':
+ if (isNull()) {
+ return decodeNull() == null ? ValueContainer.NULL : ValueContainer.NULL;
+ } else {
+ value = decodeStringLax();
+ }
+
+ break;
+
+ case '[':
+ value = decodeJsonArrayLax();
+ break;
+
+ case '{':
+ value = decodeJsonObjectLax();
+ break;
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '0':
+ return decodeNumberLax(false);
+
+ case '-':
+ return decodeNumberLax(true);
+
+ default:
+ value = decodeStringLax();
+ }
+
+ if (value != null) {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ private void handleBashComment() {
+ for (; __index < charArray.length; __index++) {
+ __currentChar = charArray[__index];
+
+ if (__currentChar == '\n') {
+ __index++;
+ return;
+ }
+ }
+ }
+
+ private void handleComment() {
+ if (hasMore()) {
+ __index++;
+ __currentChar = charArray[__index];
+
+ switch (__currentChar) {
+ case '*':
+ for (; __index < charArray.length; __index++) {
+ __currentChar = charArray[__index];
+
+ if (__currentChar == '*') {
+ if (hasMore()) {
+ __index++;
+ __currentChar = charArray[__index];
+ if (__currentChar == '/') {
+ if (hasMore()) {
+ __index++;
+ return;
+ }
+ }
+ } else {
+ complain("missing close of comment");
+ }
+ }
+ }
+
+ case '/':
+ for (; __index < charArray.length; __index++) {
+ __currentChar = charArray[__index];
+
+ if (__currentChar == '\n') {
+ if (hasMore()) {
+ __index++;
+ return;
+ } else {
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Decodes a number from a JSON value. If at any point it is determined that
+ * the value is not a valid number the value is treated as a {@code String}.
+ *
+ * @param minus indicate whether the number is negative
+ * @return a number, or {@code String} if not a valid number
+ */
+ protected final Value decodeNumberLax(boolean minus) {
+ char[] array = charArray;
+
+ final int startIndex = __index;
+ int index = __index;
+ char currentChar;
+ boolean doubleFloat = false;
+ boolean foundDot = false;
+ boolean foundSign = false;
+ boolean foundExp = false;
+
+ if (minus && index + 1 < array.length) {
+ index++;
+ }
+
+ while (true) {
+ currentChar = array[index];
+ if (isNumberDigit(currentChar)) {
+ //noop
+ } else if (currentChar <= 32) { //white
+ break;
+ } else if (isDelimiter(currentChar)) {
+ break;
+ } else if (isDecimalChar(currentChar)) {
+ switch (currentChar) {
+ case DECIMAL_POINT:
+ if (foundDot || foundExp) { return decodeStringLax(); }
+ foundDot = true;
+ break;
+ case LETTER_E:
+ case LETTER_BIG_E:
+ if (foundExp) { return decodeStringLax(); }
+ foundExp = true;
+ break;
+ case MINUS:
+ case PLUS:
+ if (foundSign || !foundExp) { return decodeStringLax(); }
+ if (foundExp && array[index - 1] != LETTER_E && array[index - 1] != LETTER_BIG_E) {
+ return decodeStringLax();
+ }
+ foundSign = true;
+ break;
+ }
+ doubleFloat = true;
+ } else {
+ return decodeStringLax();
+ }
+ index++;
+ if (index >= array.length) break;
+ }
+
+ // Handle the case where the exponential number ends without the actual exponent
+ if (foundExp) {
+ char prevChar = array[index - 1];
+ if (prevChar == LETTER_E || prevChar == LETTER_BIG_E || prevChar == MINUS || prevChar == PLUS) {
+ return decodeStringLax();
+ }
+ }
+
+ __index = index;
+ __currentChar = currentChar;
+
+ Type type = doubleFloat ? Type.DOUBLE : Type.INTEGER;
+
+ return new NumberValue(chop, type, startIndex, __index, this.charArray);
+ }
+
+ private boolean isNull() {
+ if (__index + NULL.length <= charArray.length) {
+ if (charArray[__index] == 'n' &&
+ charArray[__index + 1] == 'u' &&
+ charArray[__index + 2] == 'l' &&
+ charArray[__index + 3] == 'l') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isTrue() {
+ if (__index + TRUE.length <= charArray.length) {
+ if (charArray[__index] == 't' &&
+ charArray[__index + 1] == 'r' &&
+ charArray[__index + 2] == 'u' &&
+ charArray[__index + 3] == 'e') {
+ return true;
+
+ }
+ }
+ return false;
+ }
+
+ private boolean isFalse() {
+ if (__index + FALSE.length <= charArray.length) {
+ if (charArray[__index] == 'f' &&
+ charArray[__index + 1] == 'a' &&
+ charArray[__index + 2] == 'l' &&
+ charArray[__index + 3] == 's' &&
+ charArray[__index + 4] == 'e') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Value decodeStringLax() {
+ int index = __index;
+ char currentChar = charArray[__index];
+ final int startIndex = __index;
+ boolean encoded = false;
+ char[] charArray = this.charArray;
+
+ for (; index < charArray.length; index++) {
+ currentChar = charArray[index];
+
+ if (isDelimiter(currentChar)) break;
+ else if (currentChar == '\\') break;
+ }
+
+ Value value = this.extractLaxString(startIndex, index, encoded, defaultCheckDates);
+
+ __index = index;
+ return value;
+ }
+
+ private Value decodeStringDouble() {
+ __currentChar = charArray[__index];
+
+ if (__index < charArray.length && __currentChar == '"') {
+ __index++;
+ }
+
+ final int startIndex = __index;
+
+ boolean escape = false;
+ boolean encoded = false;
+
+ done:
+ for (; __index < this.charArray.length; __index++) {
+ __currentChar = charArray[__index];
+ switch (__currentChar) {
+
+ case '"':
+ if (!escape) {
+ break done;
+ } else {
+ escape = false;
+ continue;
+ }
+
+ case '\\':
+ escape = !escape;
+ encoded = true;
+ continue;
+ }
+ escape = false;
+ }
+
+ Value value = new CharSequenceValue(chop, Type.STRING, startIndex, __index, this.charArray, encoded, defaultCheckDates);
+
+ if (__index < charArray.length) {
+ __index++;
+ }
+
+ return value;
+ }
+
+ private Value decodeStringSingle() {
+ __currentChar = charArray[__index];
+
+ if (__index < charArray.length && __currentChar == '\'') {
+ __index++;
+ }
+
+ final int startIndex = __index;
+
+ boolean escape = false;
+ boolean encoded = false;
+ int minusCount = 0;
+ int colonCount = 0;
+
+ done:
+ for (; __index < this.charArray.length; __index++) {
+ __currentChar = charArray[__index];
+ switch (__currentChar) {
+ case '\'':
+ if (!escape) {
+ break done;
+ } else {
+ escape = false;
+ continue;
+ }
+
+ case '\\':
+ encoded = true;
+ escape = true;
+ continue;
+
+ case '-':
+ minusCount++;
+ break;
+
+ case ':':
+ colonCount++;
+ break;
+ }
+ escape = false;
+ }
+
+ boolean checkDates = defaultCheckDates && !encoded && minusCount >= 2 && colonCount >= 2;
+
+ Value value = new CharSequenceValue(chop, Type.STRING, startIndex, __index, this.charArray, encoded, checkDates);
+
+ if (__index < charArray.length) {
+ __index++;
+ }
+
+ return value;
+ }
+
+ private Value decodeJsonArrayLax() {
+ if (__currentChar == '[') {
+ __index++;
+ }
+
+ skipWhiteSpace();
+
+ if (__currentChar == ']') {
+ __index++;
+ return new ValueContainer(new ArrayList());
+ }
+
+ List<Object> list;
+
+ if (useValues) {
+ list = new ArrayList<Object>();
+ } else {
+ list = new ValueList(lazyChop);
+ }
+
+ Value value = new ValueContainer(list);
+
+ do {
+ skipWhiteSpace();
+
+ Object arrayItem = decodeValueInternal();
+
+ list.add(arrayItem);
+
+ boolean doStop = false;
+
+ done:
+ do { // Find either next array element or end of array while ignoring comments
+ skipWhiteSpace();
+
+ switch (__currentChar) {
+ case '/':
+ handleComment();
+ continue;
+ case '#':
+ handleBashComment();
+ continue;
+ case ',':
+ __index++;
+ break done;
+ case ']':
+ __index++;
+ doStop = true;
+ break done;
+ default:
+ String charString = charDescription(__currentChar);
+
+ complain(
+ String.format("expecting a ',' or a ']', " +
+ " but got \nthe current character of %s " +
+ " on array index of %s \n", charString, list.size())
+ );
+ }
+ } while (this.hasMore());
+
+ if (doStop) break;
+
+ } while (this.hasMore());
+
+ return value;
+ }
+
+ protected final Object decodeFromChars(char[] cs) {
+ Value value = ((Value) super.decodeFromChars(cs));
+ if (value.isContainer()) {
+ return value.toValue();
+ } else {
+ return value;
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserUsingCharacterSource.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserUsingCharacterSource.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserUsingCharacterSource.java
new file mode 100644
index 0000000..8b7d969
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonParserUsingCharacterSource.java
@@ -0,0 +1,298 @@
+/*
+ * 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.groovy.json.internal;
+
+import groovy.json.JsonException;
+
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Converts an input JSON String into Java objects works with String or char array
+ * as input. Produces an Object which can be any of the basic JSON types mapped
+ * to Java.
+ * <p/>
+ *
+ * @author Rick Hightower
+ */
+public class JsonParserUsingCharacterSource extends BaseJsonParser {
+
+ private CharacterSource characterSource;
+
+ protected String exceptionDetails(String message) {
+ return characterSource.errorDetails(message);
+ }
+
+ protected final Object decodeJsonObject() {
+ LazyMap map = new LazyMap();
+
+ try {
+ CharacterSource characterSource = this.characterSource;
+
+ if (characterSource.currentChar() == '{') {
+ characterSource.nextChar();
+ }
+
+ while (characterSource.hasChar()) {
+ characterSource.skipWhiteSpace();
+
+ if (characterSource.currentChar() == DOUBLE_QUOTE) {
+ String key = decodeString();
+ //puts ("key", key);
+
+ if (internKeys) {
+ String keyPrime = internedKeysCache.get(key);
+ if (keyPrime == null) {
+ key = key.intern();
+ internedKeysCache.put(key, key);
+ } else {
+ key = keyPrime;
+ }
+ }
+
+ characterSource.skipWhiteSpace();
+ if (characterSource.currentChar() != COLON) {
+ complain("expecting current character to be : but was " + charDescription(characterSource.currentChar()) + "\n");
+ }
+
+ characterSource.nextChar();
+ characterSource.skipWhiteSpace();
+
+ Object value = decodeValue();
+
+ //puts ("key", key, "value", value);
+
+ characterSource.skipWhiteSpace();
+
+ map.put(key, value);
+ }
+
+ int ch = characterSource.currentChar();
+ if (ch == '}') {
+ characterSource.nextChar();
+ break;
+ } else if (ch == ',') {
+ characterSource.nextChar();
+ continue;
+ } else {
+ complain(
+ "expecting '}' or ',' but got current char " + charDescription(ch));
+ }
+ }
+ } catch (Exception ex) {
+ throw new JsonException(exceptionDetails("Unable to parse JSON object"), ex);
+ }
+
+ return map;
+ }
+
+ protected final void complain(String complaint) {
+ throw new JsonException(exceptionDetails(complaint));
+ }
+
+ private Object decodeValue() {
+ CharacterSource characterSource = this.characterSource;
+ Object value = null;
+ characterSource.skipWhiteSpace();
+
+ switch (characterSource.currentChar()) {
+ case '"':
+ value = decodeString();
+ break;
+
+ case 't':
+ value = decodeTrue();
+ break;
+
+ case 'f':
+ value = decodeFalse();
+ break;
+
+ case 'n':
+ value = decodeNull();
+ break;
+
+ case '[':
+ value = decodeJsonArray();
+ break;
+
+ case '{':
+ value = decodeJsonObject();
+ break;
+
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ value = decodeNumber(false);
+ break;
+
+ case '-':
+ value = decodeNumber(true);
+ break;
+
+ default:
+ throw new JsonException(exceptionDetails("Unable to determine the " +
+ "current character, it is not a string, number, array, or object"));
+ }
+
+ return value;
+ }
+
+ private Object decodeNumber(boolean negative) {
+ char[] chars = characterSource.readNumber();
+ Object value = null;
+
+ if (CharScanner.hasDecimalChar(chars, negative)) {
+ value = CharScanner.parseBigDecimal(chars);
+ } else if (CharScanner.isInteger(chars)) {
+ value = CharScanner.parseInt(chars);
+ } else if (CharScanner.isLong(chars)) {
+ value = CharScanner.parseLong(chars);
+ }
+
+ return value;
+ }
+
+ protected static final char[] NULL = Chr.chars("null");
+
+ protected final Object decodeNull() {
+ if (!characterSource.consumeIfMatch(NULL)) {
+ throw new JsonException(exceptionDetails("null not parse properly"));
+ }
+ return null;
+ }
+
+ protected static final char[] TRUE = Chr.chars("true");
+
+ protected final boolean decodeTrue() {
+ if (characterSource.consumeIfMatch(TRUE)) {
+ return true;
+ } else {
+ throw new JsonException(exceptionDetails("true not parsed properly"));
+ }
+ }
+
+ protected static char[] FALSE = Chr.chars("false");
+
+ protected final boolean decodeFalse() {
+ if (characterSource.consumeIfMatch(FALSE)) {
+ return false;
+ } else {
+ throw new JsonException(exceptionDetails("false not parsed properly"));
+ }
+ }
+
+ private CharBuf builder = CharBuf.create(20);
+
+ private String decodeString() {
+ CharacterSource characterSource = this.characterSource;
+
+ characterSource.nextChar();
+
+ char[] chars = characterSource.findNextChar('"', '\\');
+
+ String value = null;
+ if (characterSource.hadEscape()) {
+ value = builder.decodeJsonString(chars).toString();
+ builder.recycle();
+ } else {
+ value = new String(chars);
+ }
+
+ return value;
+ }
+
+ protected final List decodeJsonArray() {
+ ArrayList<Object> list = null;
+
+ boolean foundEnd = false;
+ try {
+ CharacterSource characterSource = this.characterSource;
+
+ if (this.characterSource.currentChar() == '[') {
+ characterSource.nextChar();
+ }
+
+ characterSource.skipWhiteSpace();
+
+ /* the list might be empty */
+ if (this.characterSource.currentChar() == ']') {
+ characterSource.nextChar();
+ return new ArrayList();
+ }
+
+ list = new ArrayList();
+
+ do {
+ characterSource.skipWhiteSpace();
+
+ Object arrayItem = decodeValue();
+
+ list.add(arrayItem);
+
+ characterSource.skipWhiteSpace();
+
+ int c = characterSource.currentChar();
+
+ if (c == COMMA) {
+ characterSource.nextChar();
+ continue;
+ } else if (c == CLOSED_BRACKET) {
+ foundEnd = true;
+ characterSource.nextChar();
+ break;
+ } else {
+ String charString = charDescription(c);
+
+ complain(
+ String.format("expecting a ',' or a ']', " +
+ " but got \nthe current character of %s " +
+ " on array index of %s \n", charString, list.size())
+ );
+
+ }
+ } while (characterSource.hasChar());
+ } catch (Exception ex) {
+ throw new JsonException(exceptionDetails("Unexpected issue"), ex);
+ }
+
+ if (!foundEnd) {
+ throw new JsonException(exceptionDetails("Could not find end of JSON array"));
+ }
+ return list;
+ }
+
+ public Object parse(Reader reader) {
+ characterSource = new ReaderCharacterSource(reader);
+ return this.decodeValue();
+ }
+
+ public Object parse(char[] chars) {
+ return parse(new StringReader(new String(chars)));
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonStringDecoder.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonStringDecoder.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonStringDecoder.java
new file mode 100644
index 0000000..a53c9fa
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/JsonStringDecoder.java
@@ -0,0 +1,38 @@
+/*
+ * 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.groovy.json.internal;
+
+/**
+ * @author Richard Hightower
+ */
+public class JsonStringDecoder {
+
+ public static String decode(char[] chars, int start, int to) {
+ if (!Chr.contains(chars, '\\', start, to - start)) {
+ return new String(chars, start, to - start);
+ }
+ return decodeForSure(chars, start, to);
+ }
+
+ public static String decodeForSure(char[] chars, int start, int to) {
+ CharBuf builder = CharBuf.create(to - start);
+ builder.decodeJsonString(chars, start, to);
+ return builder.toString();
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyMap.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyMap.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyMap.java
new file mode 100644
index 0000000..81da4d3
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyMap.java
@@ -0,0 +1,205 @@
+/*
+ * 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.groovy.json.internal;
+
+import java.lang.reflect.Array;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * This maps only builds once you ask for a key for the first time.
+ * It is designed to not incur the overhead of creating a map unless needed.
+ *
+ * @author Rick Hightower
+ */
+public class LazyMap extends AbstractMap<String, Object> {
+
+ static final String JDK_MAP_ALTHASHING_SYSPROP = System.getProperty("jdk.map.althashing.threshold");
+
+ /* Holds the actual map that will be lazily created. */
+ private Map<String, Object> map;
+ /* The size of the map. */
+ private int size;
+ /* The keys stored in the map. */
+ private String[] keys;
+ /* The values stored in the map. */
+ private Object[] values;
+
+ public LazyMap() {
+ keys = new String[5];
+ values = new Object[5];
+ }
+
+ public LazyMap(int initialSize) {
+ keys = new String[initialSize];
+ values = new Object[initialSize];
+ }
+
+ public Object put(String key, Object value) {
+ if (map == null) {
+ for (int i = 0; i < size; i++) {
+ String curKey = keys[i];
+ if ((key == null && curKey == null)
+ || (key != null && key.equals(curKey))) {
+ Object val = values[i];
+ keys[i] = key;
+ values[i] = value;
+ return val;
+ }
+ }
+ keys[size] = key;
+ values[size] = value;
+ size++;
+ if (size == keys.length) {
+ keys = grow(keys);
+ values = grow(values);
+ }
+ return null;
+ } else {
+ return map.put(key, value);
+ }
+ }
+
+ public Set<Entry<String, Object>> entrySet() {
+ buildIfNeeded();
+ return map.entrySet();
+ }
+
+ public int size() {
+ if (map == null) {
+ return size;
+ } else {
+ return map.size();
+ }
+ }
+
+ public boolean isEmpty() {
+ if (map == null) {
+ return size == 0;
+ } else {
+ return map.isEmpty();
+ }
+ }
+
+ public boolean containsValue(Object value) {
+ buildIfNeeded();
+ return map.containsValue(value);
+ }
+
+ public boolean containsKey(Object key) {
+ buildIfNeeded();
+ return map.containsKey(key);
+ }
+
+ public Object get(Object key) {
+ buildIfNeeded();
+ return map.get(key);
+ }
+
+ private void buildIfNeeded() {
+ if (map == null) {
+ // added to avoid hash collision attack
+ if (Sys.is1_8OrLater() || (Sys.is1_7() && JDK_MAP_ALTHASHING_SYSPROP != null)) {
+ map = new LinkedHashMap<String, Object>(size, 0.01f);
+ } else {
+ map = new TreeMap<String, Object>();
+ }
+
+ for (int index = 0; index < size; index++) {
+ map.put(keys[index], values[index]);
+ }
+ this.keys = null;
+ this.values = null;
+ }
+ }
+
+ public Object remove(Object key) {
+ buildIfNeeded();
+ return map.remove(key);
+ }
+
+ public void putAll(Map m) {
+ buildIfNeeded();
+ map.putAll(m);
+ }
+
+ public void clear() {
+ if (map == null) {
+ size = 0;
+ } else {
+ map.clear();
+ }
+ }
+
+ public Set<String> keySet() {
+ buildIfNeeded();
+ return map.keySet();
+ }
+
+ public Collection<Object> values() {
+ buildIfNeeded();
+ return map.values();
+ }
+
+ public boolean equals(Object o) {
+ buildIfNeeded();
+ return map.equals(o);
+ }
+
+ public int hashCode() {
+ buildIfNeeded();
+ return map.hashCode();
+ }
+
+ public String toString() {
+ buildIfNeeded();
+ return map.toString();
+ }
+
+ protected Object clone() throws CloneNotSupportedException {
+ if (map == null) {
+ return null;
+ } else {
+ if (map instanceof LinkedHashMap) {
+ return ((LinkedHashMap) map).clone();
+ } else {
+ return new LinkedHashMap(this);
+ }
+ }
+ }
+
+ public LazyMap clearAndCopy() {
+ LazyMap map = new LazyMap();
+ for (int index = 0; index < size; index++) {
+ map.put(keys[index], values[index]);
+ }
+ size = 0;
+ return map;
+ }
+
+ public static <V> V[] grow(V[] array) {
+ Object newArray = Array.newInstance(array.getClass().getComponentType(), array.length * 2);
+ System.arraycopy(array, 0, newArray, 0, array.length);
+ return (V[]) newArray;
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyValueMap.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyValueMap.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyValueMap.java
new file mode 100644
index 0000000..7692388
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/LazyValueMap.java
@@ -0,0 +1,247 @@
+/*
+ * 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.groovy.json.internal;
+
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import static org.apache.groovy.json.internal.Exceptions.die;
+
+/**
+ * This class is important to the performance of the parser.
+ * It stores Value objects in a map where they are evaluated lazily.
+ * This is great for JSONPath types of application, and Object Serialization but not for maps that are going to be stored in a cache.
+ * <p/>
+ * This is because the Value construct is a type of index overlay that merely tracks where the token is located in the buffer,
+ * and what if any thing we noted about it (like can be converted to a decimal number, etc.).
+ * <p/>
+ * To mitigate memory leaks this class along with CharSequenceValue implement two constructs, namely,
+ * chop, and lazyChop.
+ * <p/>
+ * A chop is when we convert backing buffer of a Value object into a smaller buffer.
+ * A lazyChop is when we do a chop but only when a get operation is called.
+ * <p/>
+ * The lazyChop is performed on the tree that is touched by the JSONPath expression or its ilk.
+ * <p/>
+ * The chop operation can be done during parsing or lazily by storing the values in this construct.
+ *
+ * @author Rick Hightower (insane chipmonk)
+ */
+public class LazyValueMap extends AbstractMap<String, Object> implements ValueMap<String, Object> {
+
+ /**
+ * holds the map that gets lazily created on first access.
+ */
+ private Map<String, Object> map = null;
+ /**
+ * holds the list of items that we are managing.
+ */
+ private Entry<String, Value>[] items;
+ /**
+ * Holds the current number mapping managed by this map.
+ */
+ private int len = 0;
+ /**
+ * Holds whether or not we ae in lazy chop mode or not.
+ */
+ private final boolean lazyChop;
+
+ /**
+ * Keep track if this map has already been chopped so we don't waste time trying to chop it again.
+ */
+ boolean mapChopped = false;
+
+ public LazyValueMap(boolean lazyChop) {
+ this.items = new Entry[5];
+ this.lazyChop = lazyChop;
+ }
+
+ public LazyValueMap(boolean lazyChop, int initialSize) {
+ this.items = new Entry[initialSize];
+ this.lazyChop = lazyChop;
+ }
+
+ /**
+ * Adds a new MapItemValue to the mapping.
+ *
+ * @param miv miv we are adding.
+ */
+ public final void add(MapItemValue miv) {
+ if (len >= items.length) {
+ items = LazyMap.grow(items);
+ }
+ items[len] = miv;
+ len++;
+ }
+
+ /**
+ * Gets the item by key from the mapping.
+ *
+ * @param key to lookup
+ * @return the item for the given key
+ */
+ public final Object get(Object key) {
+ Object object = null;
+
+ /* if the map is null, then we create it. */
+ if (map == null) {
+ buildMap();
+ }
+ object = map.get(key);
+
+ lazyChopIfNeeded(object);
+ return object;
+ }
+
+ /**
+ * If in lazy chop mode, and the object is a Lazy Value Map or a ValueList
+ * then we force a chop operation for each of its items.
+ */
+ private void lazyChopIfNeeded(Object object) {
+ if (lazyChop) {
+ if (object instanceof LazyValueMap) {
+ LazyValueMap m = (LazyValueMap) object;
+ m.chopMap();
+ } else if (object instanceof ValueList) {
+ ValueList list = (ValueList) object;
+ list.chopList();
+ }
+ }
+ }
+
+ /**
+ * Chop this map.
+ */
+ public final void chopMap() {
+ /* if it has been chopped then you have to return. */
+ if (mapChopped) {
+ return;
+ }
+ mapChopped = true;
+
+ /* If the internal map was not create yet, don't. We can chop the value w/o creating the internal map.*/
+ if (this.map == null) {
+ for (int index = 0; index < len; index++) {
+ MapItemValue entry = (MapItemValue) items[index];
+
+ Value value = entry.getValue();
+ if (value == null) continue;
+ if (value.isContainer()) {
+ chopContainer(value);
+ } else {
+ value.chop();
+ }
+ }
+ } else {
+ /* Iterate through the map and do the same thing. Make sure children and children of children are chopped. */
+ for (Map.Entry<String, Object> entry : map.entrySet()) {
+
+ Object object = entry.getValue();
+ if (object instanceof Value) {
+ Value value = (Value) object;
+ if (value.isContainer()) {
+ chopContainer(value);
+ } else {
+ value.chop();
+ }
+ } else if (object instanceof LazyValueMap) {
+ LazyValueMap m = (LazyValueMap) object;
+ m.chopMap();
+ } else if (object instanceof ValueList) {
+ ValueList list = (ValueList) object;
+ list.chopList();
+ }
+ }
+ }
+ }
+
+ /* We need to chop up this child container. */
+ private static void chopContainer(Value value) {
+ Object obj = value.toValue();
+ if (obj instanceof LazyValueMap) {
+ LazyValueMap map = (LazyValueMap) obj;
+ map.chopMap();
+ } else if (obj instanceof ValueList) {
+ ValueList list = (ValueList) obj;
+ list.chopList();
+ }
+ }
+
+ public Value put(String key, Object value) {
+ die("Not that kind of map");
+ return null;
+ }
+
+ public Set<Entry<String, Object>> entrySet() {
+ if (map == null) {
+ buildMap();
+ }
+ return map.entrySet();
+ }
+
+ private void buildMap() {
+ // added to avoid hash collision attack
+ if (Sys.is1_8OrLater() || (Sys.is1_7() && LazyMap.JDK_MAP_ALTHASHING_SYSPROP != null)) {
+ map = new HashMap<String, Object>(items.length);
+ } else {
+ map = new TreeMap<String, Object>();
+ }
+
+ for (Entry<String, Value> miv : items) {
+ if (miv == null) {
+ break;
+ }
+ map.put(miv.getKey(), miv.getValue().toValue());
+ }
+
+ len = 0;
+ items = null;
+ }
+
+ public Collection<Object> values() {
+ if (map == null) buildMap();
+ return map.values();
+ }
+
+ public int size() {
+ if (map == null) buildMap();
+ return map.size();
+ }
+
+ public String toString() {
+ if (map == null) buildMap();
+ return map.toString();
+ }
+
+ public int len() {
+ return len;
+ }
+
+ public boolean hydrated() {
+ return map != null;
+ }
+
+ public Entry<String, Value>[] items() {
+ return items;
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/MapItemValue.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/MapItemValue.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/MapItemValue.java
new file mode 100644
index 0000000..d57568e
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/MapItemValue.java
@@ -0,0 +1,82 @@
+/*
+ * 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.groovy.json.internal;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.apache.groovy.json.internal.Exceptions.die;
+
+/**
+ * This holds a mapping from value key to value value to maximize laziness.
+ *
+ * @author Rick Hightower
+ */
+public class MapItemValue implements Map.Entry<String, Value> {
+
+ final Value name;
+ final Value value;
+
+ private String key = null;
+
+ private static final boolean internKeys = Boolean.parseBoolean(System.getProperty("groovy.json.implementation.internKeys", "false"));
+
+ protected static final ConcurrentHashMap<String, String> internedKeysCache;
+
+ static {
+ if (internKeys) {
+ internedKeysCache = new ConcurrentHashMap<String, String>();
+ } else {
+ internedKeysCache = null;
+ }
+ }
+
+ public MapItemValue(Value name, Value value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getKey() {
+ if (key == null) {
+ if (internKeys) {
+ key = name.toString();
+
+ String keyPrime = internedKeysCache.get(key);
+ if (keyPrime == null) {
+ key = key.intern();
+ internedKeysCache.put(key, key);
+ } else {
+ key = keyPrime;
+ }
+ } else {
+ key = name.toString();
+ }
+ }
+ return key;
+ }
+
+ public Value getValue() {
+ return value;
+ }
+
+ public Value setValue(Value value) {
+ Exceptions.die("not that kind of Entry");
+ return null;
+ }
+}
http://git-wip-us.apache.org/repos/asf/groovy/blob/5a3f9996/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/NumberValue.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/NumberValue.java b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/NumberValue.java
new file mode 100644
index 0000000..525010e
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/org/apache/groovy/json/internal/NumberValue.java
@@ -0,0 +1,219 @@
+/*
+ * 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.groovy.json.internal;
+
+import groovy.json.JsonException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Date;
+
+import static java.lang.Boolean.parseBoolean;
+import static org.apache.groovy.json.internal.CharScanner.isInteger;
+import static org.apache.groovy.json.internal.CharScanner.parseDouble;
+import static org.apache.groovy.json.internal.CharScanner.parseFloat;
+import static org.apache.groovy.json.internal.CharScanner.parseIntFromTo;
+import static org.apache.groovy.json.internal.CharScanner.parseLongFromTo;
+import static org.apache.groovy.json.internal.Exceptions.die;
+import static org.apache.groovy.json.internal.Exceptions.sputs;
+
+/**
+ * @author Rick Hightower
+ */
+public class NumberValue extends java.lang.Number implements Value {
+
+ private char[] buffer;
+ private boolean chopped;
+ private int startIndex;
+ private int endIndex;
+ private final Type type;
+ private Object value;
+
+ public NumberValue(Type type) {
+ this.type = type;
+ }
+
+ public NumberValue() {
+ this.type = null;
+ }
+
+ public NumberValue(boolean chop, Type type, int startIndex, int endIndex, char[] buffer) {
+ this.type = type;
+
+ try {
+ if (chop) {
+ this.buffer = ArrayUtils.copyRange(buffer, startIndex, endIndex);
+ this.startIndex = 0;
+ this.endIndex = this.buffer.length;
+ chopped = true;
+ } else {
+ this.startIndex = startIndex;
+ this.endIndex = endIndex;
+ this.buffer = buffer;
+ }
+ } catch (Exception ex) {
+ Exceptions.handle(sputs("exception", ex, "start", startIndex, "end", endIndex), ex);
+ }
+
+ // Check for a single minus now, rather than finding out later during lazy parsing.
+ if (this.endIndex - this.startIndex == 1 && this.buffer[this.startIndex] == '-') {
+ die("A single minus is not a valid number");
+ }
+
+ }
+
+ public String toString() {
+ if (startIndex == 0 && endIndex == buffer.length) {
+ return FastStringUtils.noCopyStringFromChars(buffer);
+ } else {
+ return new String(buffer, startIndex, (endIndex - startIndex));
+ }
+ }
+
+ public final Object toValue() {
+ return value != null ? value : (value = doToValue());
+ }
+
+ public <T extends Enum> T toEnum(Class<T> cls) {
+ return toEnum(cls, intValue());
+ }
+
+ public static <T extends Enum> T toEnum(Class<T> cls, int value) {
+ T[] enumConstants = cls.getEnumConstants();
+ for (T e : enumConstants) {
+ if (e.ordinal() == value) {
+ return e;
+ }
+ }
+ die("Can't convert ordinal value " + value + " into enum of type " + cls);
+ return null;
+ }
+
+ public boolean isContainer() {
+ return false;
+ }
+
+ private Object doToValue() {
+ switch (type) {
+ case DOUBLE:
+ return bigDecimalValue();
+ case INTEGER:
+ if (isInteger(buffer, startIndex, endIndex - startIndex)) {
+ return intValue();
+ } else {
+ return longValue();
+ }
+ }
+ die();
+ return null;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Value)) return false;
+
+ NumberValue value1 = (NumberValue) o;
+
+ if (endIndex != value1.endIndex) return false;
+ if (startIndex != value1.startIndex) return false;
+ if (!Arrays.equals(buffer, value1.buffer)) return false;
+ if (type != value1.type) return false;
+ return value != null ? value.equals(value1.value) : value1.value == null;
+
+ }
+
+ public int hashCode() {
+ int result = type != null ? type.hashCode() : 0;
+ result = 31 * result + (buffer != null ? Arrays.hashCode(buffer) : 0);
+ result = 31 * result + startIndex;
+ result = 31 * result + endIndex;
+ result = 31 * result + (value != null ? value.hashCode() : 0);
+ return result;
+ }
+
+ public BigDecimal bigDecimalValue() {
+ try {
+ return new BigDecimal(buffer, startIndex, endIndex - startIndex);
+ } catch (NumberFormatException e) {
+ throw new JsonException("unable to parse " + new String(buffer, startIndex, endIndex - startIndex), e);
+ }
+ }
+
+ public BigInteger bigIntegerValue() {
+ return new BigInteger(toString());
+ }
+
+ public String stringValue() {
+ return toString();
+ }
+
+ public String stringValueEncoded() {
+ return toString();
+ }
+
+ public Date dateValue() {
+ return new Date(Dates.utc(longValue()));
+ }
+
+ public int intValue() {
+ return parseIntFromTo(buffer, startIndex, endIndex);
+ }
+
+ public long longValue() {
+ if (isInteger(buffer, startIndex, endIndex - startIndex)) {
+ return parseIntFromTo(buffer, startIndex, endIndex);
+ } else {
+ return parseLongFromTo(buffer, startIndex, endIndex);
+ }
+ }
+
+ public byte byteValue() {
+ return (byte) intValue();
+ }
+
+ public short shortValue() {
+ return (short) intValue();
+ }
+
+ public double doubleValue() {
+ return parseDouble(this.buffer, startIndex, endIndex);
+ }
+
+ public boolean booleanValue() {
+ return parseBoolean(toString());
+ }
+
+ public float floatValue() {
+ return parseFloat(this.buffer, startIndex, endIndex);
+ }
+
+ public final void chop() {
+ if (!chopped) {
+ this.chopped = true;
+ this.buffer = ArrayUtils.copyRange(buffer, startIndex, endIndex);
+ this.startIndex = 0;
+ this.endIndex = this.buffer.length;
+ }
+ }
+
+ public char charValue() {
+ return buffer[startIndex];
+ }
+}