/*
* Copyright © 2011 Jason J.A. Stephenson
*
* This file is part of sigio.jar.
*
* sigio.jar is free software: you can redistribute it and/or modify it
* under the terms of the Lesser GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sigio.jar is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Lesser GNU General Public License for more details.
*
* You should have received a copy of the Lesser GNU General Public License
* along with sigio.jar. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sigio.json;
import java.io.PushbackReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ResourceBundle;
/**
* Reader subclass to read JSON data and create JSON objects.
*/
public class JSONReader extends PushbackReader {
/**
* A special Character constant to return if the reader hits EOF.
*/
public static final Character EOF = new Character((char)-1);
private ResourceBundle bundle = null;
/*
* Internal state variables.
*/
// Our position in the input.
private int index = 0;
/**
* Create a new JSONReader with a default pushback buffer. The
* default can pushback 1 character at a time.
*/
public JSONReader(Reader in) {
super(in);
bundle = com.sigio.json.BundleLoader.getBundle();
}
/**
* Create a new JSONReader with a specified Pushback buffer size.
*/
public JSONReader(Reader in, int size) {
super(in, size);
bundle = com.sigio.json.BundleLoader.getBundle();
}
/**
* Specialized read method to read JSON objects from the input
* data.
*
* @return object representing the JSON value read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
public Object readValue() throws IOException, JSONException {
int c = this.skipWSRead();
switch(c) {
case -1:
return JSONReader.EOF;
case JSON.BEGIN_ARRAY:
return this.readArray();
case JSON.BEGIN_OBJECT:
return this.readObject();
case JSON.QUOTE_CHAR:
return this.readString();
default:
this.unread(c);
return this.readLiteralOrNumber();
}
}
/**
* Reads a string from a JSON input. This is used by
* <code>readValue</code> and is not intended to be used by client
* code.
*
* @return string read from the inpu
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private String readString() throws IOException, JSONException {
StringBuffer sb = new StringBuffer();
while (true) {
int c = this.read();
switch (c) {
case -1:
case '\n':
case '\r':
throw this.syntaxException(this.bundle.getString("UNTERMINATED_STRING"));
case JSON.ESCAPE_CHAR:
c = this.read();
switch (c) {
case JSON.QUOTE_CHAR:
case JSON.ESCAPE_CHAR:
case '/':
sb.append((char)c);
break;
case 'b':
sb.append('\b');
break;
case 'f':
sb.append('\f');
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
case 'u':
char buf[] = new char[4];
if (this.read(buf, 0, 4) < 4) {
String message = String.format(this.bundle.getString("INVALID_ESCAPE"), "\\u" + new String(buf));
throw this.syntaxException(message);
}
sb.append((char)Integer.parseInt(new String(buf), 16));
break;
default:
String message = String.format(this.bundle.getString("INVALID_ESCAPE"), "\\" + (char)c);
throw this.syntaxException(message);
}
break;
default:
if (c == JSON.QUOTE_CHAR)
return sb.toString();
else
sb.append((char)c);
break;
}
}
}
/**
* Reads a JSONLiteral or a Number from a JSON input. This is used
* by <code>readValue</code> and is not intended to be used by
* client code.
*
* @return object read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private Object readLiteralOrNumber() throws IOException, JSONException {
StringBuffer sb = new StringBuffer();
boolean cont = true;
while (cont) {
int c = this.read();
switch (c) {
case JSON.BEGIN_ARRAY:
case JSON.BEGIN_OBJECT:
case JSON.QUOTE_CHAR:
case JSON.ESCAPE_CHAR:
case JSON.NAME_SEPARATOR:
String message = String.format(this.bundle.getString("ILLEGAL_CHARACTER"), (char) c);
throw this.syntaxException(message);
case JSON.VALUE_SEPARATOR:
case JSON.END_ARRAY:
case JSON.END_OBJECT:
this.unread(c); // Deliberate fall thru.
case -1:
cont = false;
break;
default:
if (JSON.isWhiteSpace(c))
cont = false;
else
sb.append((char)c);
break;
}
}
String str = sb.toString();
if (JSON.isNumber(str)) {
if (str.matches(".*?(?:\\.|e|E).*?")) {
try {
Double d = Double.valueOf(str);
return d;
} catch (NumberFormatException e) {
this.index -= str.length();
JSONException exc = this.syntaxException("NumberFormatException " + e.getMessage());
this.index += str.length();
throw exc;
}
} else {
try {
Long l = Long.valueOf(str);
return l;
} catch (NumberFormatException e) {
this.index -= str.length();
JSONException exc = this.syntaxException("NumberFormatException " + e.getMessage());
this.index += str.length();
throw exc;
}
}
} else {
try {
return JSONLiteral.fromString(str);
} catch (JSONException e) {
this.index -= str.length();
JSONException exc = this.syntaxException(e.getMessage());
this.index += str.length();
throw exc;
}
}
}
/**
* Reads a JSON array member from a JSON input. This is used by
* <code>readArray</code> and is not intended to be used by client
* code.
*
* @return object read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private Object readArrayValue() throws IOException, JSONException {
Object obj = this.readValue();
int c = this.skipWSRead();
if (c == JSON.VALUE_SEPARATOR || c == JSON.END_ARRAY) {
if (c == JSON.END_ARRAY)
this.unread(c);
return obj;
}
else {
String message = String.format(this.bundle.getString("ILLEGAL_CHARACTER"), (char)c);
throw this.syntaxException(message);
}
}
/**
* Reads a JSONArray from a JSON input. This is used by
* <code>readValue</code> and is not intended to be used by client
* code.
*
* @return JSONArray read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private JSONArray readArray() throws IOException, JSONException {
JSONArray jsonArray = new JSONArray();
while (this.peek() != JSON.END_ARRAY) {
jsonArray.add(this.readArrayValue());
}
this.read();
return jsonArray;
}
/**
* Reads a JSONObject from a JSON input. This is used by
* <code>readValue</code> and is not intended to be used by client
* code.
*
* @return JSONObject read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private JSONObject readObject() throws IOException, JSONException {
JSONObject jsonObject = new JSONObject();
while (this.peek() != JSON.END_OBJECT) {
String key = this.readObjectFieldName();
Object value = this.readObjectFieldValue();
jsonObject.put(key, value);
}
this.read();
return jsonObject;
}
/**
* Reads a field name for a JSON object member from a JSON
* input. This is used by <code>readObject</code> and is not
* intended to be used by client code.
*
* @return string read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private String readObjectFieldName() throws IOException, JSONException {
int c = this.skipWSRead();
if (c != JSON.QUOTE_CHAR)
throw this.syntaxException("Illegal character " + (char) c);
String name = this.readString();
c = this.skipWSRead();
if (c == JSON.NAME_SEPARATOR)
return name;
else {
String message = String.format(this.bundle.getString("ILLEGAL_CHARACTER"), (char)c);
throw this.syntaxException(message);
}
}
/**
* Reads a value of a JSON object member from a JSON input. This
* is used by <code>readObject</code> and is not intended to be
* used by client code.
*
* @return object read from the input
* @throws IOException if a read error occurs.
* @throws JSONException if the input is not properly formed JSON
* according to RFC4627
*/
private Object readObjectFieldValue() throws IOException, JSONException {
Object value = this.readValue();
int c = this.skipWSRead();
if (c == JSON.VALUE_SEPARATOR || c == JSON.END_OBJECT) {
if (c == JSON.END_OBJECT)
this.unread(c);
return value;
}
else {
String message = String.format(this.bundle.getString("ILLEGAL_CHARACTER"), (char)c);
throw this.syntaxException(message);
}
}
/**
* A special method to take advantage of the features of our
* parent PushbackWriter. It will read the next character from the
* input, push it back on the buffer and then return it.
*
* @return integer representing the character read from the input
* @throws IOException if a read error occurs.
*/
public int peek() throws IOException {
int c = this.read();
this.unread(c);
return c;
}
/**
* Reads a single character.
*
* @return The character read, or -1 if the end of stream has been
* reached.
* @throws IOException If an I/O error occurs
*/
@Override
public int read() throws IOException {
int c = super.read();
this.index++;
return c;
}
/**
* Reads characters into a portion of an array.
*
* @param buf Destination buffer
* @param off Offset at which to start writing characters
* @param len Maximum number of characters to read
* @return The number of characters read, or -1 if the end of the
* stream has been reached
* @throws IOException If an I/O error occurs
*/
@Override
public int read(char buf[], int off, int len) throws IOException {
int n = super.read(buf, off, len);
if (n > -1)
this.index += n;
return n;
}
/**
* Pushes back a single character by copying it to the front of
* the pushback buffer. After this method returns, the next
* character to be read will have the value {@code (char)c}.
*
* @param c The int value representing a character to be pushed back
* @throws If the pushback buffer is full, or if some other I/O
* error occurs
*/
@Override
public void unread(int c) throws IOException {
super.unread(c);
this.index--;
}
/**
* Pushes back a portion of an array of characters by copying it
* to the front of the pushback buffer. After this method returns,
* the next character to be read will have the value {@code
* cbuf[off]}, the character after that will have the value {@code
* cbuf[off+1]}, and so forth.
*
* @param buf Character array to push back
* @param off Offset of first character to push back
* @param len Number of characters to push back
* @throws IOException If there is insufficient room in the
* pushback buffer, or if some other I/O error occurs
*/
@Override
public void unread(char buf[], int off, int len) throws IOException {
super.unread(buf, off, len);
this.index -= len;
}
/**
* Pushes back an array of characters by copying it to the front
* of the pushback buffer. After this method returns, the next
* character to be read will have the value {@code buf[0]}, the
* character after that will have the value {@code buf[1]}, and so
* forth.
*
* @param buf Character array to push back
* @throws IOException If there is insufficient room in the
* pushback buffer, or if some other I/O error occurs
*/
@Override
public void unread(char buf[]) throws IOException {
super.unread(buf);
this.index -= buf.length;
}
/*
* Implementation method to read characters from the input until a
* non-whitespace character is found.
*/
private int skipWSRead() throws IOException {
int c = this.read();
while (JSON.isWhiteSpace(c))
c = this.read();
return c;
}
/*
* Helper method to construct a JSONException to be thrown when an
* error is detected in the input.
*/
private JSONException syntaxException(String message) {
String location = String.format(this.bundle.getString("AT"), this.index);
return new JSONException(message + " " + location);
}
}