/* * Copyright 2008-2009 LinkedIn, Inc * * 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 * * 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 voldemort.serialization.json; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import voldemort.serialization.SerializationException; /** * Read in JSON objects from a java.io.Reader * * */ public class JsonReader { // The java.io.Reader to use to get characters private final Reader reader; // the character to use to count line breaks private final char lineBreak; // the current line number private int line; // the index into the current line private int lineOffset; // the number of characters read private int charsRead; // the current character private int current; // a circular buffer of recent characters for error message context private final char[] contextBuffer; // the offset into the contextBuffer where we are at currently. private int contextOffset; public JsonReader(Reader reader) { this(reader, 50); } public JsonReader(Reader reader, int contextBufferSize) { this.reader = reader; String newline = System.getProperty("line.separator"); if(newline.contains("\n")) lineBreak = '\n'; else lineBreak = '\r'; this.line = 1; this.lineOffset = 0; this.charsRead = 0; // initialize contextBuffer to all whitespace this.contextBuffer = new char[contextBufferSize]; for(int i = 0; i < contextBufferSize; i++) this.contextBuffer[i] = ' '; next(); } public boolean hasMore() { return current() != -1; } public Object read() { skipWhitespace(); Object o = null; switch(current()) { case '{': o = readObject(); break; case '[': o = readArray(); break; case '"': case '\'': o = readString(); break; case 't': case 'f': o = readBoolean(); break; case 'n': o = readNull(); break; case -1: throw new EndOfFileException(); default: if(Character.isDigit(current()) || current() == '-') { o = readNumber(); } else { throw new SerializationException("Unacceptable initial character " + currentChar() + " found when parsing object at line " + getCurrentLineNumber() + " character " + getCurrentLineOffset()); } } skipWhitespace(); return o; } public Map<String, ?> readObject() { skip('{'); skipWhitespace(); Map<String, Object> values = new HashMap<String, Object>(); while(current() != '}') { skipWhitespace(); String key = readString(); skipWhitespace(); skip(':'); skipWhitespace(); Object value = read(); values.put(key, value); skipWhitespace(); if(current() == ',') { next(); skipWhitespace(); } else if(current() == '}') { break; } else { throw new SerializationException("Unexpected character '" + currentChar() + "' in object definition, expected '}' or ',' but found: " + getCurrentContext()); } } skip('}'); return values; } public List<?> readArray() { skip('['); skipWhitespace(); List<Object> l = new ArrayList<Object>(); while(current() != ']' && hasMore()) { l.add(read()); skipWhitespace(); if(current() == ',') { next(); skipWhitespace(); } } skip(']'); return l; } public Object readNull() { skip("null"); return null; } public Boolean readBoolean() { if(current() == 't') { skip("true"); return Boolean.TRUE; } else { skip("false"); return Boolean.FALSE; } } public String readString() { int quote = current(); StringBuilder buffer = new StringBuilder(); next(); while(current() != quote && hasMore()) { if(current() == '\\') appendControlSequence(buffer); else buffer.append(currentChar()); next(); } skip(quote); return buffer.toString(); } private void appendControlSequence(StringBuilder buffer) { skip('\\'); switch(current()) { case '"': case '\\': case '/': buffer.append(currentChar()); break; case 'b': buffer.append('\b'); break; case 'f': buffer.append('\f'); break; case 'n': buffer.append('\n'); break; case 'r': buffer.append('\r'); break; case 't': buffer.append('\t'); break; case 'u': buffer.append(readUnicodeLiteral()); break; default: throw new SerializationException("Unrecognized control sequence on line " + getCurrentLineNumber() + ": '\\" + currentChar() + "'."); } } private char readUnicodeLiteral() { int value = 0; for(int i = 0; i < 4; i++) { next(); value <<= 4; if(Character.isDigit(current())) value += current() - '0'; else if('a' <= current() && 'f' >= current()) value += 10 + current() - 'a'; else if('A' <= current() && 'F' >= current()) value += 10 + current() - 'A'; else throw new SerializationException("Invalid character in unicode sequence on line " + getCurrentLineNumber() + ": " + currentChar()); } return (char) value; } public Number readNumber() { skipWhitespace(); int intPiece = readInt(); // if int is all we have, return it if(isTerminator(current())) return intPiece; // okay its a double, check for exponent double doublePiece = intPiece; if(current() == '.') doublePiece += readFraction(); if(current() == 'e' || current() == 'E') { next(); skipIf('+'); int frac = readInt(); doublePiece *= Math.pow(10, frac); } if(isTerminator(current())) return doublePiece; else throw new SerializationException("Invalid number format for number on line " + lineOffset + ": " + getCurrentContext()); } private boolean isTerminator(int ch) { return Character.isWhitespace(ch) || ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ',' || ch == -1; } public int readInt() { skipWhitespace(); int val = 0; boolean isPositive; if(current() == '-') { isPositive = false; next(); } else if(current() == '+') { isPositive = true; next(); } else { isPositive = true; } skipWhitespace(); if(!Character.isDigit(current())) throw new SerializationException("Expected a digit while trying to parse number, but got '" + currentChar() + "' at line " + getCurrentLineNumber() + " character " + getCurrentLineOffset() + ": " + getCurrentContext()); while(Character.isDigit(current())) { val *= 10; val += (current() - '0'); next(); } if(!isPositive) val = -val; return val; } public double readFraction() { skip('.'); double position = 0.1; double val = 0; while(Character.isDigit(current())) { val += position * (current() - '0'); position *= 0.1; next(); } return val; } private int current() { return this.current; } private char currentChar() { return (char) this.current; } private int next() { try { // read a character this.current = this.reader.read(); // increment the character count and maybe line number this.charsRead++; this.lineOffset++; if(this.current == this.lineBreak) { this.line++; this.lineOffset = 1; } // add to context buffer this.contextBuffer[this.contextOffset] = (char) this.current; this.contextOffset = (contextOffset + 1) % this.contextBuffer.length; return this.current; } catch(IOException e) { throw new SerializationException("Error reading from JSON stream.", e); } } private void skipIf(char c) { if(current() == c) next(); } private void skip(String s) { for(int i = 0; i < s.length(); i++) skip(s.charAt(i)); } private void skipWhitespace() { while(Character.isWhitespace(current())) next(); } private void skip(int c) { if(current() != c) throw new SerializationException("Expected '" + ((char) c) + "' but current character is '" + currentChar() + "' on line " + line + " character " + lineOffset + ": " + getCurrentContext()); next(); } public int getCurrentLineNumber() { return this.line; } public int getCurrentLineOffset() { return this.lineOffset; } public String getCurrentContext() { StringBuilder builder = new StringBuilder(this.contextBuffer.length); for(int i = this.contextOffset; i < this.contextBuffer.length; i++) builder.append(this.contextBuffer[i]); for(int i = 0; i < contextOffset; i++) builder.append(this.contextBuffer[i]); if(this.charsRead < this.contextBuffer.length) return builder.toString().substring(this.contextBuffer.length - this.charsRead, this.contextBuffer.length); else return builder.toString(); } }