/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://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.springframework.boot.configurationprocessor.json;

// Note: this class was written without inspecting the non-free org.json source code.

Parses a JSON (RFC 4627) encoded string into the corresponding object. Most clients of this class will use only need the constructor and nextValue method. Example usage:
String json = "{"
        + "  \"query\": \"Pizza\", "
        + "  \"locations\": [ 94043, 90210 ] "
        + "}";
JSONObject object = (JSONObject) new JSONTokener(json).nextValue();
String query = object.getString("query");
JSONArray locations = object.getJSONArray("locations");

For best interoperability and performance use JSON that complies with RFC 4627, such as that generated by JSONStringer. For legacy reasons this parser is lenient, so a successful parse does not indicate that the input string was valid JSON. All of the following syntax errors will be ignored:

  • End of line comments starting with // or # and ending with a newline character.
  • C-style comments starting with /* and ending with */. Such comments may not be nested.
  • Strings that are unquoted or 'single quoted'.
  • Hexadecimal integers prefixed with 0x or 0X.
  • Octal integers prefixed with 0.
  • Array elements separated by ;.
  • Unnecessary array separators. These are interpreted as if null was the omitted value.
  • Key-value pairs separated by = or =>.
  • Key-value pairs separated by ;.

Each tokener may be used to parse a single JSON string. Instances of this class are not thread safe. Although this class is nonfinal, it was not designed for inheritance and should not be subclassed. In particular, self-use by overrideable methods is not specified. See Effective Java Item 17, "Design and Document or inheritance or else prohibit it" for further information.

/** * Parses a JSON (<a href="https://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) encoded * string into the corresponding object. Most clients of this class will use only need the * {@link #JSONTokener(String) constructor} and {@link #nextValue} method. Example usage: * <pre> * String json = "{" * + " \"query\": \"Pizza\", " * + " \"locations\": [ 94043, 90210 ] " * + "}"; * * JSONObject object = (JSONObject) new JSONTokener(json).nextValue(); * String query = object.getString("query"); * JSONArray locations = object.getJSONArray("locations");</pre> * <p> * For best interoperability and performance use JSON that complies with RFC 4627, such as * that generated by {@link JSONStringer}. For legacy reasons this parser is lenient, so a * successful parse does not indicate that the input string was valid JSON. All of the * following syntax errors will be ignored: * <ul> * <li>End of line comments starting with {@code //} or {@code #} and ending with a * newline character. * <li>C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such * comments may not be nested. * <li>Strings that are unquoted or {@code 'single quoted'}. * <li>Hexadecimal integers prefixed with {@code 0x} or {@code 0X}. * <li>Octal integers prefixed with {@code 0}. * <li>Array elements separated by {@code ;}. * <li>Unnecessary array separators. These are interpreted as if null was the omitted * value. * <li>Key-value pairs separated by {@code =} or {@code =>}. * <li>Key-value pairs separated by {@code ;}. * </ul> * <p> * Each tokener may be used to parse a single JSON string. Instances of this class are not * thread safe. Although this class is nonfinal, it was not designed for inheritance and * should not be subclassed. In particular, self-use by overrideable methods is not * specified. See <i>Effective Java</i> Item 17, "Design and Document or inheritance or * else prohibit it" for further information. */
public class JSONTokener {
The input JSON.
/** * The input JSON. */
private final String in;
The index of the next character to be returned by next. When the input is exhausted, this equals the input's length.
/** * The index of the next character to be returned by {@link #next}. When the input is * exhausted, this equals the input's length. */
private int pos;
Params:
  • in – JSON encoded string. Null is not permitted and will yield a tokener that throws NullPointerExceptions when methods are called.
/** * @param in JSON encoded string. Null is not permitted and will yield a tokener that * throws {@code NullPointerExceptions} when methods are called. */
public JSONTokener(String in) { // consume an optional byte order mark (BOM) if it exists if (in != null && in.startsWith("\ufeff")) { in = in.substring(1); } this.in = in; }
Returns the next value from the input.
Throws:
Returns:a JSONObject, JSONArray, String, Boolean, Integer, Long, Double or JSONObject.NULL.
/** * Returns the next value from the input. * @return a {@link JSONObject}, {@link JSONArray}, String, Boolean, Integer, Long, * Double or {@link JSONObject#NULL}. * @throws JSONException if the input is malformed. */
public Object nextValue() throws JSONException { int c = nextCleanInternal(); switch (c) { case -1: throw syntaxError("End of input"); case '{': return readObject(); case '[': return readArray(); case '\'': case '"': return nextString((char) c); default: this.pos--; return readLiteral(); } } private int nextCleanInternal() throws JSONException { while (this.pos < this.in.length()) { int c = this.in.charAt(this.pos++); switch (c) { case '\t': case ' ': case '\n': case '\r': continue; case '/': if (this.pos == this.in.length()) { return c; } char peek = this.in.charAt(this.pos); switch (peek) { case '*': // skip a /* c-style comment */ this.pos++; int commentEnd = this.in.indexOf("*/", this.pos); if (commentEnd == -1) { throw syntaxError("Unterminated comment"); } this.pos = commentEnd + 2; continue; case '/': // skip a // end-of-line comment this.pos++; skipToEndOfLine(); continue; default: return c; } case '#': /* * Skip a # hash end-of-line comment. The JSON RFC doesn't specify this * behavior, but it's required to parse existing documents. See * https://b/2571423. */ skipToEndOfLine(); continue; default: return c; } } return -1; }
Advances the position until after the next newline character. If the line is terminated by "\r\n", the '\n' must be consumed as whitespace by the caller.
/** * Advances the position until after the next newline character. If the line is * terminated by "\r\n", the '\n' must be consumed as whitespace by the caller. */
private void skipToEndOfLine() { for (; this.pos < this.in.length(); this.pos++) { char c = this.in.charAt(this.pos); if (c == '\r' || c == '\n') { this.pos++; break; } } }
Returns the string up to but not including quote, unescaping any character escape sequences encountered along the way. The opening quote should have already been read. This consumes the closing quote, but does not include it in the returned string.
Params:
  • quote – either ' or ".
Throws:
Returns:the string up to but not including quote
/** * Returns the string up to but not including {@code quote}, unescaping any character * escape sequences encountered along the way. The opening quote should have already * been read. This consumes the closing quote, but does not include it in the returned * string. * @param quote either ' or ". * @return the string up to but not including {@code quote} * @throws NumberFormatException if any unicode escape sequences are malformed. * @throws JSONException if processing of json failed */
public String nextString(char quote) throws JSONException { /* * For strings that are free of escape sequences, we can just extract the result * as a substring of the input. But if we encounter an escape sequence, we need to * use a StringBuilder to compose the result. */ StringBuilder builder = null; /* the index of the first character not yet appended to the builder. */ int start = this.pos; while (this.pos < this.in.length()) { int c = this.in.charAt(this.pos++); if (c == quote) { if (builder == null) { // a new string avoids leaking memory return new String(this.in.substring(start, this.pos - 1)); } else { builder.append(this.in, start, this.pos - 1); return builder.toString(); } } if (c == '\\') { if (this.pos == this.in.length()) { throw syntaxError("Unterminated escape sequence"); } if (builder == null) { builder = new StringBuilder(); } builder.append(this.in, start, this.pos - 1); builder.append(readEscapeCharacter()); start = this.pos; } } throw syntaxError("Unterminated string"); }
Unescapes the character identified by the character or characters that immediately follow a backslash. The backslash '\' should have already been read. This supports both unicode escapes "u000A" and two-character escapes "\n".
Throws:
Returns:the unescaped char
/** * Unescapes the character identified by the character or characters that immediately * follow a backslash. The backslash '\' should have already been read. This supports * both unicode escapes "u000A" and two-character escapes "\n". * @return the unescaped char * @throws NumberFormatException if any unicode escape sequences are malformed. * @throws JSONException if processing of json failed */
private char readEscapeCharacter() throws JSONException { char escaped = this.in.charAt(this.pos++); switch (escaped) { case 'u': if (this.pos + 4 > this.in.length()) { throw syntaxError("Unterminated escape sequence"); } String hex = this.in.substring(this.pos, this.pos + 4); this.pos += 4; return (char) Integer.parseInt(hex, 16); case 't': return '\t'; case 'b': return '\b'; case 'n': return '\n'; case 'r': return '\r'; case 'f': return '\f'; case '\'': case '"': case '\\': default: return escaped; } }
Reads a null, boolean, numeric or unquoted string literal value. Numeric values will be returned as an Integer, Long, or Double, in that order of preference.
Throws:
Returns:a literal value
/** * Reads a null, boolean, numeric or unquoted string literal value. Numeric values * will be returned as an Integer, Long, or Double, in that order of preference. * @return a literal value * @throws JSONException if processing of json failed */
private Object readLiteral() throws JSONException { String literal = nextToInternal("{}[]/\\:,=;# \t\f"); if (literal.isEmpty()) { throw syntaxError("Expected literal value"); } else if ("null".equalsIgnoreCase(literal)) { return JSONObject.NULL; } else if ("true".equalsIgnoreCase(literal)) { return Boolean.TRUE; } else if ("false".equalsIgnoreCase(literal)) { return Boolean.FALSE; } /* try to parse as an integral type... */ if (literal.indexOf('.') == -1) { int base = 10; String number = literal; if (number.startsWith("0x") || number.startsWith("0X")) { number = number.substring(2); base = 16; } else if (number.startsWith("0") && number.length() > 1) { number = number.substring(1); base = 8; } try { long longValue = Long.parseLong(number, base); if (longValue <= Integer.MAX_VALUE && longValue >= Integer.MIN_VALUE) { return (int) longValue; } else { return longValue; } } catch (NumberFormatException e) { /* * This only happens for integral numbers greater than Long.MAX_VALUE, * numbers in exponential form (5e-10) and unquoted strings. Fall through * to try floating point. */ } } /* ...next try to parse as a floating point... */ try { return Double.valueOf(literal); } catch (NumberFormatException ignored) { } /* ... finally give up. We have an unquoted string */ return new String(literal); // a new string avoids leaking memory }
Returns the string up to but not including any of the given characters or a newline character. This does not consume the excluded character.
Returns:the string up to but not including any of the given characters or a newline character
/** * Returns the string up to but not including any of the given characters or a newline * character. This does not consume the excluded character. * @return the string up to but not including any of the given characters or a newline * character */
private String nextToInternal(String excluded) { int start = this.pos; for (; this.pos < this.in.length(); this.pos++) { char c = this.in.charAt(this.pos); if (c == '\r' || c == '\n' || excluded.indexOf(c) != -1) { return this.in.substring(start, this.pos); } } return this.in.substring(start); }
Reads a sequence of key/value pairs and the trailing closing brace '}' of an object. The opening brace '{' should have already been read.
Throws:
Returns:an object
/** * Reads a sequence of key/value pairs and the trailing closing brace '}' of an * object. The opening brace '{' should have already been read. * @return an object * @throws JSONException if processing of json failed */
private JSONObject readObject() throws JSONException { JSONObject result = new JSONObject(); /* Peek to see if this is the empty object. */ int first = nextCleanInternal(); if (first == '}') { return result; } else if (first != -1) { this.pos--; } while (true) { Object name = nextValue(); if (!(name instanceof String)) { if (name == null) { throw syntaxError("Names cannot be null"); } else { throw syntaxError( "Names must be strings, but " + name + " is of type " + name.getClass().getName()); } } /* * Expect the name/value separator to be either a colon ':', an equals sign * '=', or an arrow "=>". The last two are bogus but we include them because * that's what the original implementation did. */ int separator = nextCleanInternal(); if (separator != ':' && separator != '=') { throw syntaxError("Expected ':' after " + name); } if (this.pos < this.in.length() && this.in.charAt(this.pos) == '>') { this.pos++; } result.put((String) name, nextValue()); switch (nextCleanInternal()) { case '}': return result; case ';': case ',': continue; default: throw syntaxError("Unterminated object"); } } }
Reads a sequence of values and the trailing closing brace ']' of an array. The opening brace '[' should have already been read. Note that "[]" yields an empty array, but "[,]" returns a two-element array equivalent to "[null,null]".
Throws:
Returns:an array
/** * Reads a sequence of values and the trailing closing brace ']' of an array. The * opening brace '[' should have already been read. Note that "[]" yields an empty * array, but "[,]" returns a two-element array equivalent to "[null,null]". * @return an array * @throws JSONException if processing of json failed */
private JSONArray readArray() throws JSONException { JSONArray result = new JSONArray(); /* to cover input that ends with ",]". */ boolean hasTrailingSeparator = false; while (true) { switch (nextCleanInternal()) { case -1: throw syntaxError("Unterminated array"); case ']': if (hasTrailingSeparator) { result.put(null); } return result; case ',': case ';': /* A separator without a value first means "null". */ result.put(null); hasTrailingSeparator = true; continue; default: this.pos--; } result.put(nextValue()); switch (nextCleanInternal()) { case ']': return result; case ',': case ';': hasTrailingSeparator = true; continue; default: throw syntaxError("Unterminated array"); } } }
Returns an exception containing the given message plus the current position and the entire input string.
Params:
  • message – the message
Returns:an exception
/** * Returns an exception containing the given message plus the current position and the * entire input string. * @param message the message * @return an exception */
public JSONException syntaxError(String message) { return new JSONException(message + this); }
Returns the current position and the entire input string.
Returns:the current position and the entire input string.
/** * Returns the current position and the entire input string. * @return the current position and the entire input string. */
@Override public String toString() { // consistent with the original implementation return " at character " + this.pos + " of " + this.in; } /* * Legacy APIs. * * None of the methods below are on the critical path of parsing JSON documents. They * exist only because they were exposed by the original implementation and may be used * by some clients. */ public boolean more() { return this.pos < this.in.length(); } public char next() { return this.pos < this.in.length() ? this.in.charAt(this.pos++) : '\0'; } public char next(char c) throws JSONException { char result = next(); if (result != c) { throw syntaxError("Expected " + c + " but was " + result); } return result; } public char nextClean() throws JSONException { int nextCleanInt = nextCleanInternal(); return nextCleanInt == -1 ? '\0' : (char) nextCleanInt; } public String next(int length) throws JSONException { if (this.pos + length > this.in.length()) { throw syntaxError(length + " is out of bounds"); } String result = this.in.substring(this.pos, this.pos + length); this.pos += length; return result; } public String nextTo(String excluded) { if (excluded == null) { throw new NullPointerException("excluded == null"); } return nextToInternal(excluded).trim(); } public String nextTo(char excluded) { return nextToInternal(String.valueOf(excluded)).trim(); } public void skipPast(String thru) { int thruStart = this.in.indexOf(thru, this.pos); this.pos = thruStart == -1 ? this.in.length() : (thruStart + thru.length()); } public char skipTo(char to) { int index = this.in.indexOf(to, this.pos); if (index != -1) { this.pos = index; return to; } else { return '\0'; } } public void back() { if (--this.pos == -1) { this.pos = 0; } } public static int dehexchar(char hex) { if (hex >= '0' && hex <= '9') { return hex - '0'; } else if (hex >= 'A' && hex <= 'F') { return hex - 'A' + 10; } else if (hex >= 'a' && hex <= 'f') { return hex - 'a' + 10; } else { return -1; } } }