All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.eatthepath.json.JsonParser Maven / Gradle / Ivy

package com.eatthepath.json;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 

An extremely simple JSON parser that interprets JSON objects as Java primitives. JSON types are mapped to Java * types as follows:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Mapping of JSON types to Java types
JSON typeJava type
string{@link String}
number{@link Number} ({@link Long} or {@link Double})
object{@link Map}<{@link String}, {@link Object}>
array{@link List}<{@link Object}>
booleanboolean
nullnull
* *

{@code JsonParser} instances are not thread-safe; only one thread may safely use a single parser at a * time, but parsers can be reused (i.e. callers do not need to construct a new parser for each parsing operation).

* * @author Jon Chambers * * @since 0.14.1 */ public class JsonParser { private transient String jsonString; private transient int position; private transient final StringBuilder stringBuilder = new StringBuilder(); /** * Constructs a new JSON parser. Parsers are not thread-safe, but may be reused. */ public JsonParser() {} /** * Parses the given JSON string as a JSON object. * * @param jsonString the JSON string to parse * * @return a Map representing the JSON object * * @throws ParseException if the given JSON string could not be parsed as a JSON object for any reason */ public Map parseJsonObject(final String jsonString) throws ParseException { this.jsonString = jsonString; this.position = findNextToken(jsonString, 0); if (jsonString.charAt(position) != '{') { throw new ParseException("JSON string does not represent a JSON object.", position); } return parseObject(); } /** * Parses and returns the JSON value (which may be of any JSON type) at the current read position and advances the * read position to the character after the end of the value. * * @return the value at the current read position */ Object parseNextValue() throws ParseException { position = findNextToken(jsonString, position); final char c = jsonString.charAt(position); final Object value; if (c == '"') { value = parseString(); } else if (c == '-' || (c >= '0' && c <= '9')) { final int numberStart = position; try { value = parseNumber(); } catch (final NumberFormatException e) { throw new ParseException("Could not parse number", numberStart); } } else if (c == '{') { value = parseObject(); } else if (c == '[') { value = parseList(); } else if (c == 't') { if (jsonString.regionMatches(position, "true", 0, 4)) { value = true; position += 4; } else { throw new ParseException("Unexpected literal", position); } } else if (c == 'f') { if (jsonString.regionMatches(position, "false", 0, 5)) { value = false; position += 5; } else { throw new ParseException("Unexpected literal", position); } } else if (c == 'n') { if (jsonString.regionMatches(position, "null", 0, 4)) { value = null; position += 4; } else { throw new ParseException("Unexpected literal", position); } } else { throw new ParseException("Unexpected token", position); } return value; } /** * Parses and returns the string at the current read position and advances the read position to the character after * the end of the string. * * @return the string at the current read position */ String parseString() throws ParseException { // Reset and reuse the string builder to avoid unnecessary allocations stringBuilder.setLength(0); // Skip the opening quote position += 1; int start = position; for (; position < jsonString.length(); position++) { final char c = jsonString.charAt(position); if (c == '\\') { final int charsConsumed; final char escapedChar; if (position + 1 >= jsonString.length()) { throw new ParseException("Expected escaped character, but reached end of input", position); } switch (jsonString.charAt(position + 1)) { case '\"': { charsConsumed = 1; escapedChar = '\"'; break; } case '\\': { charsConsumed = 1; escapedChar = '\\'; break; } case '/': { charsConsumed = 1; escapedChar = '/'; break; } case 'b': { charsConsumed = 1; escapedChar = '\b'; break; } case 'f': { charsConsumed = 1; escapedChar = '\f'; break; } case 'n': { charsConsumed = 1; escapedChar = '\n'; break; } case 'r': { charsConsumed = 1; escapedChar = '\r'; break; } case 't': { charsConsumed = 1; escapedChar = '\t'; break; } case 'u': { if (position + 5 >= jsonString.length()) { throw new ParseException("Expected escaped unicode sequence, but reached end of input", position); } charsConsumed = 5; escapedChar = (char) Integer.parseInt(jsonString.substring(position + 2, position + 6), 16); break; } default: { throw new ParseException("Illegal escaped character: " + jsonString.charAt(position + 1), position); } } stringBuilder.append(jsonString, start, position); stringBuilder.append(escapedChar); position += charsConsumed; start = position + 1; } else if (c == '"') { // If the string builder is empty, we didn't wind up un-escaping anything and can just return a // substring to save an array copy (original string -> returned string as opposed to original string -> // string builder -> returned string). final String unescapedString; if (stringBuilder.length() == 0) { unescapedString = jsonString.substring(start, position); } else { stringBuilder.append(jsonString, start, position); unescapedString = stringBuilder.toString(); } // Position the read index after the end of the string position += 1; return unescapedString; } } throw new ParseException("Expected \", but reached end of input.", position); } /** * Parses and returns the number at the current read position and advances the read position to the character after * the end of the number token. * * @return the number at the current read position */ Number parseNumber() { final boolean negate = jsonString.charAt(position) == '-'; boolean parseAsDouble = false; final int startInclusive = position; int endExclusive; long parsedLong = 0; for (endExclusive = negate ? position + 1 : position; endExclusive < jsonString.length(); endExclusive++) { final char c = jsonString.charAt(endExclusive); if (c >= '0' && c <= '9') { parsedLong *= 10; parsedLong += c - '0'; } else if (c == '.' || c == 'e' || c == 'E' || c == '+' || (c == '-')) { parseAsDouble = true; } else { break; } } position = endExclusive; if (parseAsDouble) { return Double.parseDouble(jsonString.substring(startInclusive, endExclusive)); } else { return negate ? -parsedLong : parsedLong; } } /** * Parses and returns the object at the current read position and advances the read position to the character after * the end of the object. * * @return the object at the current read position */ Map parseObject() throws ParseException { // Advance past the opening bracket position = findNextToken(jsonString, position + 1); final Map map = new HashMap<>(); while (jsonString.charAt(position) != '}') { if (jsonString.charAt(position) != '"') { throw new ParseException("Expected a string", position); } final String key = parseString(); position = findNextToken(jsonString, position); if (jsonString.charAt(position) != ':') { throw new ParseException("Expected ':'", position); } // Advance past the key/value separator position += 1; map.put(key, parseNextValue()); position = findNextToken(jsonString, position); if (jsonString.charAt(position) == ',') { // Position the cursor at the start of the token after the comma position = findNextToken(jsonString, position + 1); } } // Advance past the closing bracket position += 1; return map; } /** * Parses and returns the list at the current read position and advances the read position to the character after * the end of the list. * * @return the list at the current read position */ List parseList() throws ParseException { // Advance past the opening bracket position = findNextToken(jsonString, position + 1); final List list = new ArrayList<>(); while (jsonString.charAt(position) != ']') { list.add(parseNextValue()); position = findNextToken(jsonString, position); if (jsonString.charAt(position) == ',') { // Position the cursor at the start of the token after the comma position = findNextToken(jsonString, position + 1); } } // Advance past the closing bracket position += 1; return list; } // Visible for testing void setJsonString(final String jsonString) { this.jsonString = jsonString; this.position = 0; } /** * Returns the index of the first character in the given string at or after the given start index that may be the * first character of a non-whitespace JSON token. * * @param string the string in which to find a token * @param start the first index at which to look for a non-whitespace token * * @return the index of the first non-whitespace character in the given string at or after the given start index * * @throws ParseException if the end of the string was reached before a non-whitespace token was found */ static int findNextToken(final String string, final int start) throws ParseException { for (int i = start; i < string.length(); i++) { final char c = string.charAt(i); if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) { return i; } } throw new ParseException("Expected token, but reached end of input", start); } }