Home / Class/ JSONTokener Class — spring-boot Architecture

JSONTokener Class — spring-boot Architecture

Architecture documentation for the JSONTokener class in JSONTokener.java from the spring-boot codebase.

Entity Profile

Source Code

cli/spring-boot-cli/src/json-shade/java/org/springframework/boot/cli/json/JSONTokener.java lines 60–555

public class JSONTokener {

	/**
	 * The input JSON.
	 */
	private final String in;

	/**
	 * 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;

	/**
	 * @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.
	 * @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 '\'', '"':
				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', ' ', '\n', '\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.
	 */
	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 {@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".
	 * @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 '\'', '"', '\\':
			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.
	 * @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 ex) {
			// Ignore
		}

		/* ... 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.
	 * @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.
	 * @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 ';', ',':
					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]".
	 * @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 ',', ';':
					/* 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 ',', ';':
					hasTrailingSeparator = true;
					continue;
				default:
					throw syntaxError("Unterminated array");
			}
		}
	}

	/**
	 * 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.
	 * @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;
		}
	}

}

Analyze Your Own Codebase

Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.

Try Supermodel Free