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

com.dslplatform.json.JsonReader Maven / Gradle / Ivy

There is a newer version: 2.0.2
Show newest version
package com.dslplatform.json;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.*;

/**
 * Object for processing JSON from byte[] and InputStream.
 * DSL-JSON works on byte level (instead of char level).
 * Deserialized instances can obtain TContext information provided with this reader.
 * 

* JsonReader can be reused by calling process methods. * * @param context passed to deserialized object instances */ public final class JsonReader { private static final boolean[] WHITESPACE = new boolean[256]; static { WHITESPACE[9 + 128] = true; WHITESPACE[10 + 128] = true; WHITESPACE[11 + 128] = true; WHITESPACE[12 + 128] = true; WHITESPACE[13 + 128] = true; WHITESPACE[32 + 128] = true; WHITESPACE[-96 + 128] = true; WHITESPACE[-31 + 128] = true; WHITESPACE[-30 + 128] = true; WHITESPACE[-29 + 128] = true; } private int tokenStart; private int nameEnd; private int currentIndex = 0; private long currentPosition = 0; private byte last = ' '; private int length; private final char[] tmp; public final TContext context; protected byte[] buffer; protected char[] chars; private InputStream stream; private int readLimit; //always leave some room for reading special stuff, so that buffer contains enough padding for such optimizations private int bufferLenWithExtraSpace; private final StringCache keyCache; private final StringCache valuesCache; private final TypeLookup typeLookup; public enum DoublePrecision { EXACT(0), HIGH(1), DEFAULT(3), LOW(4); final int level; DoublePrecision(int level) { this.level = level; } } public enum UnknownNumberParsing { LONG_AND_BIGDECIMAL, LONG_AND_DOUBLE, BIGDECIMAL, DOUBLE } protected final DoublePrecision doublePrecision; protected final int doubleLengthLimit; protected final UnknownNumberParsing unknownNumbers; protected final int maxNumberDigits; private final int maxStringBuffer; private JsonReader( final char[] tmp, final byte[] buffer, final int length, final TContext context, final StringCache keyCache, final StringCache valuesCache, final TypeLookup typeLookup, final DoublePrecision doublePrecision, final UnknownNumberParsing unknownNumbers, final int maxNumberDigits, final int maxStringBuffer) { this.tmp = tmp; this.buffer = buffer; this.length = length; this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid this.context = context; this.chars = tmp; this.keyCache = keyCache; this.valuesCache = valuesCache; this.typeLookup = typeLookup; this.doublePrecision = doublePrecision; this.unknownNumbers = unknownNumbers; this.maxNumberDigits = maxNumberDigits; this.maxStringBuffer = maxStringBuffer; this.doubleLengthLimit = 15 + doublePrecision.level; } /** * Prefer creating reader through DslJson#newReader since it will pass several arguments (such as key/string value cache) * First byte will not be read. * It will allocate new char[64] for string buffer. * Key and string vales cache will be null. * * @param buffer input JSON * @param context context */ @Deprecated public JsonReader(final byte[] buffer, final TContext context) { this(buffer, context, null, null); } @Deprecated public JsonReader(final byte[] buffer, final TContext context, StringCache keyCache, StringCache valuesCache) { this(buffer, buffer.length, context, new char[64], keyCache, valuesCache); } @Deprecated public JsonReader(final byte[] buffer, final TContext context, final char[] tmp) { this(buffer, buffer.length, context, tmp); if (tmp == null) { throw new IllegalArgumentException("tmp buffer provided as null."); } } @Deprecated public JsonReader(final byte[] buffer, final int length, final TContext context) { this(buffer, length, context, new char[64]); } @Deprecated public JsonReader(final byte[] buffer, final int length, final TContext context, final char[] tmp) { this(buffer, length, context, tmp, null, null); } @Deprecated public JsonReader(final byte[] buffer, final int length, final TContext context, final char[] tmp, final StringCache keyCache, final StringCache valuesCache) { this(tmp, buffer, length, context, keyCache, valuesCache, null, DoublePrecision.DEFAULT, UnknownNumberParsing.LONG_AND_BIGDECIMAL, 512, 256 * 1024 * 1024); if (tmp == null) { throw new IllegalArgumentException("tmp buffer provided as null."); } if (length > buffer.length) { throw new IllegalArgumentException("length can't be longer than buffer.length"); } else if (length < buffer.length) { buffer[length] = '\0'; } } JsonReader( final byte[] buffer, final int length, final TContext context, final char[] tmp, final StringCache keyCache, final StringCache valuesCache, final TypeLookup typeLookup, final DoublePrecision doublePrecision, final UnknownNumberParsing unknownNumbers, final int maxNumberDigits, final int maxStringBuffer) { this(tmp, buffer, length, context, keyCache, valuesCache, typeLookup, doublePrecision, unknownNumbers, maxNumberDigits, maxStringBuffer); if (tmp == null) { throw new IllegalArgumentException("tmp buffer provided as null."); } if (length > buffer.length) { throw new IllegalArgumentException("length can't be longer than buffer.length"); } else if (length < buffer.length) { buffer[length] = '\0'; } } /** * Will be removed. Exists only for backward compatibility * @param stream process stream * @throws IOException error reading from stream */ @Deprecated public final void reset(final InputStream stream) throws IOException { process(stream); } /** * Will be removed. Exists only for backward compatibility * @param size size of byte[] input to use */ @Deprecated final void reset(final int size) { process(null, size); } /** * Bind input stream for processing. * Stream will be processed in byte[] chunks. * If stream is null, reference to stream will be released. * * @param stream set input stream * @return itself * @throws IOException unable to read from stream */ public final JsonReader process(final InputStream stream) throws IOException { this.currentPosition = 0; this.currentIndex = 0; this.stream = stream; if (stream != null) { this.readLimit = this.length < bufferLenWithExtraSpace ? this.length : bufferLenWithExtraSpace; final int available = readFully(buffer, stream, 0); readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; this.length = available; } return this; } /** * Bind byte[] buffer for processing. * If this method is used in combination with process(InputStream) this buffer will be used for processing chunks of stream. * If null is sent for byte[] buffer, new length for valid input will be set for existing buffer. * * @param newBuffer new buffer to use for processing * @param newLength length of buffer which can be used * @return itself */ public final JsonReader process(final byte[] newBuffer, final int newLength) { if (newBuffer != null) { this.buffer = newBuffer; this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid } if (newLength > buffer.length) { throw new IllegalArgumentException("length can't be longer than buffer.length"); } currentIndex = 0; this.length = newLength; this.stream = null; return this; } /** * Valid length of the input buffer. * * @return size of JSON input */ public final int length() { return length; } private final static Charset UTF_8 = Charset.forName("UTF-8"); @Override public String toString() { return new String(buffer, 0, length, UTF_8); } private static int readFully(final byte[] buffer, final InputStream stream, final int offset) throws IOException { int read; int position = offset; while (position < buffer.length && (read = stream.read(buffer, position, buffer.length - position)) != -1) { position += read; } return position; } /** * Read next byte from the JSON input. * If buffer has been read in full IOException will be thrown * * @return next byte * @throws IOException when end of JSON input */ public final byte read() throws IOException { if (stream != null && currentIndex > readLimit) { prepareNextBlock(); } if (currentIndex >= length) { throw new IOException("Unexpected end of JSON input"); } return last = buffer[currentIndex++]; } private int prepareNextBlock() throws IOException { final int len = length - currentIndex; System.arraycopy(buffer, currentIndex, buffer, 0, len); final int available = readFully(buffer, stream, len); currentPosition += currentIndex; if (available == len) { readLimit = length - currentIndex; length = readLimit; currentIndex = 0; } else { readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; this.length = available; currentIndex = 0; } return available; } final boolean isEndOfStream() throws IOException { if (stream == null) { return length == currentIndex; } if (length != currentIndex) { return false; } return prepareNextBlock() == 0; } /** * Which was last byte read from the JSON input. * JsonReader doesn't allow to go back, but it remembers previously read byte * * @return which was the last byte read */ public final byte last() { return last; } final IOException expecting(final String what) { return new IOException("Expecting '" + what + "' at position " + positionInStream() + ". Found " + (char) last); } final IOException expecting(final String what, final byte found) { return new IOException("Expecting '" + what + "' at position " + positionInStream() + ". Found " + (char) found); } public final int getTokenStart() { return tokenStart; } public final int getCurrentIndex() { return currentIndex; } /** * will be removed. not used anymore * * @return parsed chars from a number */ @Deprecated public final char[] readNumber() { tokenStart = currentIndex - 1; tmp[0] = (char) last; int i = 1; int ci = currentIndex; byte bb = last; while (i < tmp.length && ci < length) { bb = buffer[ci++]; if (bb == ',' || bb == '}' || bb == ']') break; tmp[i++] = (char) bb; } currentIndex += i - 1; last = bb; return tmp; } public final int scanNumber() { tokenStart = currentIndex - 1; int i = 1; int ci = currentIndex; byte bb = last; while (ci < length) { bb = buffer[ci++]; if (bb == ',' || bb == '}' || bb == ']') break; i++; } currentIndex += i - 1; last = bb; return tokenStart; } final char[] prepareBuffer(final int start, final int len) { while (chars.length < len) { chars = Arrays.copyOf(chars, chars.length * 2); } final char[] _tmp = chars; final byte[] _buf = buffer; for (int i = 0; i < len; i++) { _tmp[i] = (char) _buf[start + i]; } return _tmp; } final boolean allWhitespace(final int start, final int end) { final byte[] _buf = buffer; for (int i = start; i < end; i++) { if (!WHITESPACE[_buf[i] + 128]) return false; } return true; } final int findNonWhitespace(final int end) { final byte[] _buf = buffer; for (int i = end - 1; i > 0; i--) { if (!WHITESPACE[_buf[i] + 128]) return i + 1; } return 0; } /** * Read simple ascii string. Will not use values cache to create instance. * * @return parsed string * @throws IOException unable to parse string */ public final String readSimpleString() throws IOException { if (last != '"') { throw new IOException("Expecting '\"' at position " + positionInStream() + ". Found " + (char) last); } int i = 0; int ci = currentIndex; try { while (i < tmp.length) { final byte bb = buffer[ci++]; if (bb == '"') break; tmp[i++] = (char) bb; } } catch (ArrayIndexOutOfBoundsException ignore) { throw new IOException("JSON string was not closed with a double quote at: " + positionInStream()); } if (ci > length) { throw new IOException("JSON string was not closed with a double quote at: " + (currentPosition + length)); } currentIndex = ci; return new String(tmp, 0, i); } /** * Read simple "ascii string" into temporary buffer. * String length must be obtained through getTokenStart and getCurrentToken * * @return temporary buffer * @throws IOException unable to parse string */ public final char[] readSimpleQuote() throws IOException { if (last != '"') { throw new IOException("Expecting '\"' at position " + positionInStream() + ". Found " + (char) last); } int ci = tokenStart = currentIndex; try { for (int i = 0; i < tmp.length; i++) { final byte bb = buffer[ci++]; if (bb == '"') break; tmp[i] = (char) bb; } } catch (ArrayIndexOutOfBoundsException ignore) { throw new IOException("JSON string was not closed with a double quote at: " + positionInStream()); } if (ci > length) { throw new IOException("JSON string was not closed with a double quote at: " + (currentPosition + length)); } currentIndex = ci; return tmp; } /** * Read string from JSON input. * If values cache is used, string will be looked up from the cache. *

* String value must start and end with a double quote ("). * * @return parsed string * @throws IOException error reading string input */ public final String readString() throws IOException { final int len = parseString(); return valuesCache == null ? new String(chars, 0, len) : valuesCache.get(chars, len); } final int parseString() throws IOException { final int startIndex = currentIndex; if (last != '"') { //TODO: count special chars in separate counter throw new IOException("JSON string must start with a double quote at: " + positionInStream()); } else if (currentIndex == length) { throw new IOException("Unable to parse input at position " + positionInStream() + ". Premature end of JSON input"); } byte bb; int ci = currentIndex; char[] _tmp = chars; final int remaining = length - currentIndex; int _tmpLen = _tmp.length < remaining ? _tmp.length : remaining; int i = 0; while (i < _tmpLen) { bb = buffer[ci++]; if (bb == '"') { currentIndex = ci; return i; } // If we encounter a backslash, which is a beginning of an escape sequence // or a high bit was set - indicating an UTF-8 encoded multibyte character, // there is no chance that we can decode the string without instantiating // a temporary buffer, so quit this loop if ((bb ^ '\\') < 1) break; _tmp[i++] = (char) bb; } if (i == _tmp.length) { final int newSize = chars.length * 2; if (newSize > maxStringBuffer) throw new IOException("Unable to process input JSON. Maximum string buffer limit exceeded: " + maxStringBuffer); _tmp = chars = Arrays.copyOf(chars, newSize); _tmpLen = _tmp.length; } currentIndex = ci; int soFar = --currentIndex - startIndex; while (!isEndOfStream()) { int bc = read(); if (bc == '"') { return soFar; } if (bc == '\\') { if (soFar >= _tmpLen - 6) { final int newSize = chars.length * 2; if (newSize > maxStringBuffer) throw new IOException("Unable to process input JSON. Maximum string buffer limit exceeded: " + maxStringBuffer); _tmp = chars = Arrays.copyOf(chars, newSize); _tmpLen = _tmp.length; } bc = buffer[currentIndex++]; switch (bc) { case 'b': bc = '\b'; break; case 't': bc = '\t'; break; case 'n': bc = '\n'; break; case 'f': bc = '\f'; break; case 'r': bc = '\r'; break; case '"': case '/': case '\\': break; case 'u': bc = (hexToInt(buffer[currentIndex++]) << 12) + (hexToInt(buffer[currentIndex++]) << 8) + (hexToInt(buffer[currentIndex++]) << 4) + hexToInt(buffer[currentIndex++]); break; default: throw new IOException("Could not parse String at position: " + positionInStream() + ". Invalid escape combination detected: '\\" + bc + "'"); } } else if ((bc & 0x80) != 0) { if (soFar >= _tmpLen - 4) { final int newSize = chars.length * 2; if (newSize > maxStringBuffer) throw new IOException("Unable to process input JSON. Maximum string buffer limit exceeded: " + maxStringBuffer); _tmp = chars = Arrays.copyOf(chars, newSize); _tmpLen = _tmp.length; } final int u2 = buffer[currentIndex++]; if ((bc & 0xE0) == 0xC0) { bc = ((bc & 0x1F) << 6) + (u2 & 0x3F); } else { final int u3 = buffer[currentIndex++]; if ((bc & 0xF0) == 0xE0) { bc = ((bc & 0x0F) << 12) + ((u2 & 0x3F) << 6) + (u3 & 0x3F); } else { final int u4 = buffer[currentIndex++]; if ((bc & 0xF8) == 0xF0) { bc = ((bc & 0x07) << 18) + ((u2 & 0x3F) << 12) + ((u3 & 0x3F) << 6) + (u4 & 0x3F); } else { // there are legal 5 & 6 byte combinations, but none are _valid_ throw new IOException("Invalid unicode character detected at: " + positionInStream()); } if (bc >= 0x10000) { // check if valid unicode if (bc >= 0x110000) { throw new IOException("Invalid unicode character detected at: " + positionInStream()); } // split surrogates final int sup = bc - 0x10000; _tmp[soFar++] = (char) ((sup >>> 10) + 0xd800); _tmp[soFar++] = (char) ((sup & 0x3ff) + 0xdc00); } } } } else if (soFar >= _tmpLen) { final int newSize = chars.length * 2; if (newSize > maxStringBuffer) throw new IOException("Unable to process input JSON. Maximum string buffer limit exceeded: " + maxStringBuffer); _tmp = chars = Arrays.copyOf(chars, newSize); _tmpLen = _tmp.length; } _tmp[soFar++] = (char) bc; } throw new IOException("JSON string was not closed with a double quote at: " + positionInStream()); } private static int hexToInt(final byte value) throws IOException { if (value >= '0' && value <= '9') return value - 0x30; if (value >= 'A' && value <= 'F') return value - 0x37; if (value >= 'a' && value <= 'f') return value - 0x57; throw new IOException("Could not parse unicode escape, expected a hexadecimal digit, got '" + value + "'"); } private boolean wasWhiteSpace() { switch (last) { case 9: case 10: case 11: case 12: case 13: case 32: case -96: return true; case -31: if (currentIndex + 1 < length && buffer[currentIndex] == -102 && buffer[currentIndex + 1] == -128) { currentIndex += 2; last = ' '; return true; } return false; case -30: if (currentIndex + 1 < length) { final byte b1 = buffer[currentIndex]; final byte b2 = buffer[currentIndex + 1]; if (b1 == -127 && b2 == -97) { currentIndex += 2; last = ' '; return true; } if (b1 != -128) return false; switch (b2) { case -128: case -127: case -126: case -125: case -124: case -123: case -122: case -121: case -120: case -119: case -118: case -88: case -87: case -81: currentIndex += 2; last = ' '; return true; default: return false; } } else { return false; } case -29: if (currentIndex + 1 < length && buffer[currentIndex] == -128 && buffer[currentIndex + 1] == -128) { currentIndex += 2; last = ' '; return true; } return false; default: return false; } } /** * Read next token (byte) from input JSON. * Whitespace will be skipped and next non-whitespace byte will be returned. * * @return next non-whitespace byte in the JSON input * @throws IOException unable to get next byte (end of stream, ...) */ public final byte getNextToken() throws IOException { read(); if (WHITESPACE[last + 128]) { while (wasWhiteSpace()) { read(); } } return last; } public final long positionInStream() { return currentPosition + currentIndex; } public final long positionInStream(final int offset) { return currentPosition + currentIndex - offset; } public final int fillName() throws IOException { final int hash = calcHash(); if (read() != ':') { if (!wasWhiteSpace() || getNextToken() != ':') { throw new IOException("Expecting ':' at position " + positionInStream() + ". Found " + (char) last); } } return hash; } public final int calcHash() throws IOException { if (last != '"') { throw new IOException("Expecting '\"' at position " + positionInStream() + ". Found " + (char) last); } tokenStart = currentIndex; int ci = currentIndex; long hash = 0x811c9dc5; if (stream != null) { while (ci < readLimit) { final byte b = buffer[ci]; if (b == '"') break; ci++; hash ^= b; hash *= 0x1000193; } if (ci >= readLimit) { return calcHashAndCopyName(hash, ci); } nameEnd = currentIndex = ci + 1; } else { while (ci < buffer.length) { final byte b = buffer[ci++]; if (b == '"') break; hash ^= b; hash *= 0x1000193; } nameEnd = currentIndex = ci; } return (int) hash; } private int lastNameLen; private int calcHashAndCopyName(long hash, int ci) throws IOException { int soFar = ci - tokenStart; long startPosition = currentPosition - soFar; while (chars.length < soFar) { chars = Arrays.copyOf(chars, chars.length * 2); } int i = 0; for (; i < soFar; i++) { chars[i] = (char) buffer[i + tokenStart]; } currentIndex = ci; do { final byte b = read(); if (b == '"') { nameEnd = -1; lastNameLen = i; return (int) hash; } if (i == chars.length) { chars = Arrays.copyOf(chars, chars.length * 2); } chars[i++] = (char) b; hash ^= b; hash *= 0x1000193; } while (!isEndOfStream()); throw new IOException("JSON string was not closed with a double quote at: " + startPosition); } public final boolean wasLastName(final String name) { if (stream != null && nameEnd == -1) { if (name.length() != lastNameLen) { return false; } for (int i = 0; i < name.length(); i++) { if (name.charAt(i) != chars[i]) { return false; } } return true; } if (name.length() != nameEnd - tokenStart - 1) { return false; } for (int i = 0; i < name.length(); i++) { if (name.charAt(i) != buffer[tokenStart + i]) { return false; } } return true; } public final String getLastName() throws IOException { if (stream != null && nameEnd == -1) { return new String(chars, 0, lastNameLen); } return new String(buffer, tokenStart, nameEnd - tokenStart - 1, "UTF-8"); } private byte skipString() throws IOException { byte c = read(); byte prev = c; boolean inEscape = false; while (c != '"' || inEscape) { prev = c; inEscape = !inEscape && prev == '\\'; c = read(); } return getNextToken(); } /** * Skip to next non-whitespace token (byte) * Will not allocate memory while skipping over JSON input. * * @return next non-whitespace byte * @throws IOException unable to read next byte (end of stream, invalid JSON, ...) */ public final byte skip() throws IOException { if (last == '"') return skipString(); if (last == '{') { byte nextToken = getNextToken(); if (nextToken == '}') return getNextToken(); if (nextToken == '"') { nextToken = skipString(); } else { throw new IOException("Expecting '\"' at position " + positionInStream() + ". Found " + (char) nextToken); } if (nextToken != ':') { throw new IOException("Expecting ':' at position " + positionInStream() + ". Found " + (char) nextToken); } getNextToken(); nextToken = skip(); while (nextToken == ',') { nextToken = getNextToken(); if (nextToken == '"') { nextToken = skipString(); } else { throw new IOException("Expecting '\"' at position " + positionInStream() + ". Found " + (char) nextToken); } if (nextToken != ':') { throw new IOException("Expecting ':' at position " + positionInStream() + ". Found " + (char) nextToken); } getNextToken(); nextToken = skip(); } if (nextToken != '}') { throw new IOException("Expecting '}' at position " + positionInStream() + ". Found " + (char) nextToken); } return getNextToken(); } if (last == '[') { getNextToken(); byte nextToken = skip(); while (nextToken == ',') { getNextToken(); nextToken = skip(); } if (nextToken != ']') { throw new IOException("Expecting ']' at position " + positionInStream() + ". Found " + (char) nextToken); } return getNextToken(); } if (last == 'n') { if (!wasNull()) { throw new IOException("Expecting 'null' at position " + positionInStream()); } return getNextToken(); } if (last == 't') { if (!wasTrue()) { throw new IOException("Expecting 'true' at position " + positionInStream()); } return getNextToken(); } if (last == 'f') { if (!wasFalse()) { throw new IOException("Expecting 'false' at position " + positionInStream()); } return getNextToken(); } while (last != ',' && last != '}' && last != ']') { read(); } return last; } /** * will be removed * * @return not used anymore * @throws IOException throws if invalid JSON detected */ @Deprecated public String readNext() throws IOException { final int start = currentIndex - 1; skip(); return new String(buffer, start, currentIndex - start - 1, "UTF-8"); } public final byte[] readBase64() throws IOException { if (stream != null && Base64.findEnd(buffer, currentIndex) == buffer.length) { final int len = parseString(); final byte[] input = new byte[len]; for (int i = 0; i < input.length; i++) { input[i] = (byte) chars[i]; } return Base64.decodeFast(input, 0, len); } if (last != '"') { throw new IOException("Expecting '\"' at position " + positionInStream() + " at base64 start. Found " + (char) last); } final int start = currentIndex; currentIndex = Base64.findEnd(buffer, start); last = buffer[currentIndex++]; if (last != '"') { throw new IOException("Expecting '\"' at position " + positionInStream() + " at base64 end. Found " + (char) last); } return Base64.decodeFast(buffer, start, currentIndex - 1); } /** * Read key value of JSON input. * If key cache is used, it will be looked up from there. * * @return parsed key value * @throws IOException unable to parse string input */ public final String readKey() throws IOException { final int len = parseString(); final String key = keyCache != null ? keyCache.get(chars, len) : new String(chars, 0, len); if (getNextToken() != ':') { throw new IOException("Expecting ':' at position " + positionInStream() + ". Found " + (char) last); } getNextToken(); return key; } /** * Custom objects can be deserialized based on the implementation specified through this interface. * Annotation processor creates custom deserializers at compile time and registers them into DslJson. * * @param type */ public interface ReadObject { T read(JsonReader reader) throws IOException; } /** * Existing instances can be provided as target for deserialization. * Annotation processor creates custom deserializers at compile time and registers them into DslJson. * * @param type */ public interface BindObject { T bind(JsonReader reader, T instance) throws IOException; } public interface ReadJsonObject { T deserialize(JsonReader reader) throws IOException; } /** * Checks if 'null' value is at current position. * This means last read byte was 'n' and 'ull' are next three bytes. * If last byte was n but next three are not 'ull' it will throw since that is not a valid JSON construct. * * @return true if 'null' value is at current position * @throws IOException invalid 'null' value detected */ public final boolean wasNull() throws IOException { if (last == 'n') { if (currentIndex + 2 < length && buffer[currentIndex] == 'u' && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 'l') { currentIndex += 3; last = 'l'; return true; } throw new IOException("Invalid null value found at: " + positionInStream()); } return false; } /** * Checks if 'true' value is at current position. * This means last read byte was 't' and 'rue' are next three bytes. * If last byte was t but next three are not 'rue' it will throw since that is not a valid JSON construct. * * @return true if 'true' value is at current position * @throws IOException invalid 'true' value detected */ public final boolean wasTrue() throws IOException { if (last == 't') { if (currentIndex + 2 < length && buffer[currentIndex] == 'r' && buffer[currentIndex + 1] == 'u' && buffer[currentIndex + 2] == 'e') { currentIndex += 3; last = 'e'; return true; } throw new IOException("Invalid boolean value found at: " + positionInStream()); } return false; } /** * Checks if 'false' value is at current position. * This means last read byte was 'f' and 'alse' are next four bytes. * If last byte was f but next four are not 'alse' it will throw since that is not a valid JSON construct. * * @return true if 'false' value is at current position * @throws IOException invalid 'false' value detected */ public final boolean wasFalse() throws IOException { if (last == 'f') { if (currentIndex + 3 < length && buffer[currentIndex] == 'a' && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 's' && buffer[currentIndex + 3] == 'e') { currentIndex += 4; last = 'e'; return true; } throw new IOException("Invalid boolean value found at: " + positionInStream()); } return false; } /** * Will advance to next token and check if it's comma * * @throws IOException it's not comma */ public final void comma() throws IOException { if (getNextToken() != ',') { if (currentIndex >= length) throw new IOException("Unexpected end in JSON at: " + positionInStream()); else throw new IOException("Expecting ',' at position " + positionInStream() + ". Found " + (char) last); } } /** * Will advance to next token and check if it's semicolon * * @throws IOException it's not semicolon */ public final void semicolon() throws IOException { if (getNextToken() != ':') { if (currentIndex >= length) throw new IOException("Unexpected end in JSON at: " + positionInStream()); else throw new IOException("Expecting ':' at position " + positionInStream() + ". Found " + (char) last); } } /** * Will advance to next token and check if it's array start * * @throws IOException it's not array start */ public final void startArray() throws IOException { if (getNextToken() != '[') { if (currentIndex >= length) throw new IOException("Unexpected start of collection in JSON at: " + positionInStream()); else throw new IOException("Expecting '[' at position " + positionInStream() + ". Found " + (char) last); } } /** * Will advance to next token and check if it's array end * * @throws IOException it's not array end */ public final void endArray() throws IOException { if (getNextToken() != ']') { if (currentIndex >= length) throw new IOException("Unexpected end of collection in JSON at: " + positionInStream()); else throw new IOException("Expecting ']' at position " + positionInStream() + ". Found " + (char) last); } } /** * Will advance to next token and check if it's object start * * @throws IOException it's not object start */ public final void startObject() throws IOException { if (getNextToken() != '{') { if (currentIndex >= length) throw new IOException("Unexpected start of object in JSON at: " + positionInStream()); else throw new IOException("Expecting '{' at position " + positionInStream() + ". Found " + (char) last); } } /** * Will advance to next token and check it it's object end * * @throws IOException it's not object end */ public final void endObject() throws IOException { if (getNextToken() != '}') { if (currentIndex >= length) throw new IOException("Unexpected end of object in JSON at: " + positionInStream()); else throw new IOException("Expecting '}' at position " + positionInStream() + ". Found " + (char) last); } } /** * Check if the last read token is an array end * * @throws IOException it's not array end */ public final void checkArrayEnd() throws IOException { if (last != ']') { if (currentIndex >= length) throw new IOException("Unexpected end of JSON in collection at: " + positionInStream()); else throw new IOException("Expecting ']' at position " + positionInStream() + ". Found " + (char) last); } } private Object readNull(final Class manifest) throws IOException { if (!wasNull()) throw new IllegalArgumentException("Invalid JSON detected at: " + positionInStream()); if (manifest.isPrimitive()) { if (manifest == int.class) return 0; else if (manifest == long.class) return 0L; else if (manifest == short.class) return (short)0; else if (manifest == byte.class) return (byte)0; else if (manifest == float.class) return 0f; else if (manifest == double.class) return 0d; else if (manifest == boolean.class) return false; else if (manifest == char.class) return '\0'; } return null; } /** * Will advance to next token and read the JSON into specified type * * @param manifest type to read into * @param type * @return new instance from input JSON * @throws IOException unable to process JSON */ @SuppressWarnings("unchecked") public final T next(final Class manifest) throws IOException { if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); if (typeLookup == null) throw new IllegalArgumentException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); if (this.getNextToken() == 'n') { return (T)readNull(manifest); } final ReadObject reader = typeLookup.tryFindReader(manifest); if (reader == null) { throw new IllegalArgumentException("Reader not found for " + manifest + ". Check if reader was registered"); } return reader.read(this); } /** * Will advance to next token and read the JSON into specified type * * @param reader reader to use * @param type * @return new instance from input JSON * @throws IOException unable to process JSON */ public final T next(final ReadObject reader) throws IOException { if (reader == null) throw new IllegalArgumentException("reader can't be null"); if (this.getNextToken() == 'n') { if (!wasNull()) throw new IllegalArgumentException("Invalid JSON detected at: " + positionInStream()); return null; } return reader.read(this); } /** * Will advance to next token and bind the JSON to provided instance * * @param manifest type to read into * @param instance instance to bind * @param type * @return bound instance * @throws IOException unable to process JSON */ @SuppressWarnings("unchecked") public final T next(final Class manifest, final T instance) throws IOException { if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); if (instance == null) throw new IllegalArgumentException("instance can't be null"); if (typeLookup == null) throw new IllegalArgumentException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); if (this.getNextToken() == 'n') { return (T)readNull(manifest); } final BindObject binder = typeLookup.tryFindBinder(manifest); if (binder == null) throw new IllegalArgumentException("Binder not found for " + manifest + ". Check if binder was registered"); return binder.bind(this, instance); } /** * Will advance to next token and bind the JSON to provided instance * * @param binder binder to use * @param instance instance to bind * @param type * @return bound instance * @throws IOException unable to process JSON */ @SuppressWarnings("unchecked") public final T next(final BindObject binder, final T instance) throws IOException { if (binder == null) throw new IllegalArgumentException("binder can't be null"); if (instance == null) throw new IllegalArgumentException("instance can't be null"); if (this.getNextToken() == 'n') { if (!wasNull()) throw new IllegalArgumentException("Invalid JSON detected at: " + positionInStream()); return null; } return binder.bind(this, instance); } public final ArrayList deserializeCollection(final ReadObject readObject) throws IOException { final ArrayList res = new ArrayList(4); deserializeCollection(readObject, res); return res; } public final void deserializeCollection(final ReadObject readObject, final Collection res) throws IOException { res.add(readObject.read(this)); while (getNextToken() == ',') { getNextToken(); res.add(readObject.read(this)); } checkArrayEnd(); } public final ArrayList deserializeNullableCollection(final ReadObject readObject) throws IOException { final ArrayList res = new ArrayList(4); deserializeNullableCollection(readObject, res); return res; } public final void deserializeNullableCollection(final ReadObject readObject, final Collection res) throws IOException { if (wasNull()) { res.add(null); } else { res.add(readObject.read(this)); } while (getNextToken() == ',') { getNextToken(); if (wasNull()) { res.add(null); } else { res.add(readObject.read(this)); } } checkArrayEnd(); } public final ArrayList deserializeCollection(final ReadJsonObject readObject) throws IOException { final ArrayList res = new ArrayList(4); deserializeCollection(readObject, res); return res; } public final void deserializeCollection(final ReadJsonObject readObject, final Collection res) throws IOException { if (last == '{') { getNextToken(); res.add(readObject.deserialize(this)); } else throw new IOException("Expecting '{' at position " + positionInStream() + ". Found " + (char) last); while (getNextToken() == ',') { if (getNextToken() == '{') { getNextToken(); res.add(readObject.deserialize(this)); } else throw new IOException("Expecting '{' at position " + positionInStream() + ". Found " + (char) last); } checkArrayEnd(); } public final ArrayList deserializeNullableCollection(final ReadJsonObject readObject) throws IOException { final ArrayList res = new ArrayList(4); deserializeNullableCollection(readObject, res); return res; } public final void deserializeNullableCollection(final ReadJsonObject readObject, final Collection res) throws IOException { if (last == '{') { getNextToken(); res.add(readObject.deserialize(this)); } else if (wasNull()) { res.add(null); } else throw new IOException("Expecting '{' at position " + positionInStream() + ". Found " + (char) last); while (getNextToken() == ',') { if (getNextToken() == '{') { getNextToken(); res.add(readObject.deserialize(this)); } else if (wasNull()) { res.add(null); } else throw new IOException("Expecting '{' at position " + positionInStream() + ". Found " + (char) last); } checkArrayEnd(); } public final Iterator iterateOver(final JsonReader.ReadObject reader) throws IOException { return new WithReader(reader, this); } public final Iterator iterateOver(final JsonReader.ReadJsonObject reader) throws IOException { return new WithObjectReader(reader, this); } private static class WithReader implements Iterator { private final JsonReader.ReadObject reader; private final JsonReader json; private boolean hasNext; WithReader(JsonReader.ReadObject reader, JsonReader json) { this.reader = reader; this.json = json; hasNext = true; } @Override public boolean hasNext() { return hasNext; } @Override public void remove() { } @Override public T next() { try { byte nextToken = json.last(); final T instance; if (nextToken == 'n') { if (json.wasNull()) { instance = null; } else { throw json.expecting("null"); } } else { instance = reader.read(json); } hasNext = json.getNextToken() == ','; if (hasNext) { json.getNextToken(); } else { if (json.last() != ']') { throw json.expecting("]"); } //TODO: ideally we should release stream bound to reader } return instance; } catch (IOException e) { throw new SerializationException(e); } } } private static class WithObjectReader implements Iterator { private final JsonReader.ReadJsonObject reader; private final JsonReader json; private boolean hasNext; WithObjectReader(JsonReader.ReadJsonObject reader, JsonReader json) { this.reader = reader; this.json = json; hasNext = true; } @Override public boolean hasNext() { return hasNext; } @Override public void remove() { } @Override public T next() { try { byte nextToken = json.last(); final T instance; if (nextToken == 'n') { if (json.wasNull()) { instance = null; } else { throw json.expecting("null"); } } else if (nextToken == '{') { json.getNextToken(); instance = reader.deserialize(json); } else { throw json.expecting("{"); } hasNext = json.getNextToken() == ','; if (hasNext) { json.getNextToken(); } else { if (json.last() != ']') { throw json.expecting("]"); } //TODO: ideally we should release stream bound to reader } return instance; } catch (IOException e) { throw new SerializationException(e); } } } }