/*******************************************************************************
* Copyright (c) 2007, 2013 Wind River Systems, Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wind River Systems - initial API and implementation
*******************************************************************************/
package org.eclipse.tcf.protocol;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.tcf.core.Base64;
/**
* JSON is TCF preferred marshaling. This class implements generation and parsing of JSON strings.
* The code is optimized for speed since it is a time-critical part of the framework.
*
* Reading of JSON produces data structure that consists of objects of these classes:
* Boolean, Number, String, Collection, Map.
*
* Writing of JSON is supported for:
* Boolean, Number, String, char[], byte[], Object[], Collection, Map
*
* Clients can enable writing support for objects of a other classes by
* registering ObjectWriter interface implementation.
*
* @noinstantiate This class is not intended to be instantiated by clients.
*/
public final class JSON {
/**
* Clients implement ObjectWriter interface when they want to enable marshaling of
* object classes that are not directly supported by JSON library.
*/
public interface ObjectWriter<V> {
void write(V o) throws IOException;
}
private static final Map<Class<?>,ObjectWriter<?>> object_writers =
new HashMap<Class<?>,ObjectWriter<?>>();
/** Wrapper class for binary byte blocs */
public final static class Binary {
public final byte[] bytes;
public final int offs;
public final int size;
public Binary(byte[] bytes, int offs, int size) {
this.bytes = bytes;
this.offs = offs;
this.size = size;
}
}
private static char[] tmp_buf = new char[0x1000];
private static byte[] tmp_bbf = new byte[0x1000];
private static int tmp_buf_pos;
private static boolean zero_copy;
private static Binary[] bin_buf = new Binary[0x10];
private static int bin_buf_pos;
private static byte[] inp;
private static int inp_pos;
private static int cur_ch;
// This buffer is used to create nice error reports
private static final char[] err_buf = new char[100];
private static int err_buf_pos;
private static int err_buf_cnt;
/**
* Add a handler for converting objects of a particular class into JSON.
* @param cls - a class
* @param writer - ObjectWriter implementation that provides generation of JSON for a given class.
*/
public static <X> void addObjectWriter(Class<X> cls, ObjectWriter<X> writer) {
object_writers.put(cls, writer);
}
/**
* Write a character into JSON output buffer.
* Clients should not call this method directly, except from ObjectWriter implementation.
* @param ch
*/
public static void write(char ch) {
if (tmp_buf_pos >= tmp_buf.length) {
char[] tmp = new char[tmp_buf.length * 2];
System.arraycopy(tmp_buf, 0, tmp, 0, tmp_buf_pos);
tmp_buf = tmp;
}
tmp_buf[tmp_buf_pos++] = ch;
}
/**
* Write a string into JSON output buffer.
* The string is written "as-is". Call writeObject() to convert a String into JSON string.
* Clients should not call this method directly, except from ObjectWriter implementation.
* @param s - a string
*/
public static void write(String s) {
int l = s.length();
for (int i = 0; i < l; i++) {
char ch = s.charAt(i);
if (tmp_buf_pos >= tmp_buf.length) write(ch);
else tmp_buf[tmp_buf_pos++] = ch;
}
}
/**
* Write a non-negative integer number into JSON output buffer.
* Clients should not call this method directly, except from ObjectWriter implementation.
* @param n - a number
*/
public static void writeUInt(int n) {
assert n >= 0;
if (n >= 10) writeUInt(n / 10);
write((char)('0' + n % 10));
}
private static int readUTF8Char() {
if (inp_pos >= inp.length) return -1;
int ch = inp[inp_pos++];
if (ch < 0) {
int n = 0;
if ((ch & 0xe0) == 0xc0) {
ch &= 0x1f;
n = 1;
}
else if ((ch & 0xf0) == 0xe0) {
ch &= 0x0f;
n = 2;
}
else if ((ch & 0xf8) == 0xf0) {
ch &= 0x07;
n = 3;
}
else if ((ch & 0xfc) == 0xf8) {
ch &= 0x03;
n = 4;
}
else if ((ch & 0xfe) == 0xfc) {
ch &= 0x01;
n = 5;
}
while (n > 0) {
if (inp_pos >= inp.length || (inp[inp_pos] & 0xc0) != 0x80) break;
ch = (ch << 6) | (inp[inp_pos++] & 0x3f);
n--;
}
while (n > 0) {
ch = ch << 6;
n--;
}
if (ch < 0) ch = 0;
}
return ch;
}
private static void read() throws IOException {
cur_ch = readUTF8Char();
err_buf[err_buf_pos++] = (char)cur_ch;
if (err_buf_pos >= err_buf.length) {
err_buf_pos = 0;
err_buf_cnt++;
}
}
private static void skipWS() throws IOException {
while (cur_ch <= ' ') {
switch (cur_ch) {
case '\r':
case '\n':
case '\t':
case ' ':
read();
break;
default:
return;
}
}
}
private static void error() throws IOException {
error("syntax error");
}
private static void error(String msg) throws IOException {
StringBuffer bf = new StringBuffer();
bf.append("JSON " + msg + ":");
int cnt = 0;
boolean nl = true;
for (int i = 0;; i++) {
char ch = 0;
if (err_buf_cnt == 0 && i < err_buf_pos) {
ch = err_buf[i];
}
else if (err_buf_cnt > 0 && i < err_buf.length) {
ch = err_buf[(err_buf_pos + i) % err_buf.length];
}
else {
int n = readUTF8Char();
if (n < 0) break;
ch = (char)n;
}
if (nl) {
bf.append("\n ");
if (err_buf_cnt == 0) bf.append(cnt);
else bf.append('*');
bf.append(": ");
if (cnt == 0 && err_buf_cnt > 0) bf.append("...");
nl = false;
}
if (ch == 0) {
cnt++;
nl = true;
continue;
}
bf.append(ch);
}
throw new IOException(bf.toString());
}
private static int readHexDigit() throws IOException {
int n = 0;
if (cur_ch >= '0' && cur_ch <= '9') n = cur_ch - '0';
else if (cur_ch >= 'A' && cur_ch <= 'F') n = cur_ch - 'A' + 10;
else if (cur_ch >= 'a' && cur_ch <= 'f') n = cur_ch - 'a' + 10;
else error();
read();
return n;
}
private static Object readFloat(boolean sign, BigInteger val) throws IOException {
int scale = 0;
int fraction = 0;
if (cur_ch == '.') {
read();
while (cur_ch >= '0' && cur_ch <= '9') {
val = val.multiply(BigInteger.valueOf(10));
val = val.add(BigInteger.valueOf(cur_ch - '0'));
fraction++;
read();
}
}
if (cur_ch == 'E' || cur_ch == 'e') {
read();
boolean neg = cur_ch == '-';
if (neg || cur_ch == '+') read();
while (cur_ch >= '0' && cur_ch <= '9') {
scale = scale * 10 + cur_ch - '0';
read();
}
if (neg) scale = -scale;
}
if (sign) val = val.negate();
return new BigDecimal(val, fraction - scale);
}
private static Object readNestedObject() throws IOException {
skipWS();
switch (cur_ch) {
case '(':
read();
int len = 0;
while (cur_ch >= '0' && cur_ch <= '9') {
len = len * 10 + (cur_ch - '0');
read();
}
if (cur_ch != ')') error();
byte[] res = new byte[len];
System.arraycopy(inp, inp_pos, res, 0, len);
inp_pos += len;
read();
return res;
case '"':
read();
tmp_buf_pos = 0;
for (;;) {
if (cur_ch < 0) error();
if (cur_ch == '"') break;
if (cur_ch == '\\') {
read();
if (cur_ch < 0) error();
switch (cur_ch) {
case '"':
case '\\':
case '/':
break;
case 'b':
cur_ch = '\b';
break;
case 'f':
cur_ch = '\f';
break;
case 'n':
cur_ch = '\n';
break;
case 'r':
cur_ch = '\r';
break;
case 't':
cur_ch = '\t';
break;
case 'u':
read();
int n = 0;
n |= readHexDigit() << 12;
n |= readHexDigit() << 8;
n |= readHexDigit() << 4;
n |= readHexDigit();
write((char)n);
continue;
default:
error();
break;
}
}
if (tmp_buf_pos >= tmp_buf.length) {
write((char)cur_ch);
}
else {
tmp_buf[tmp_buf_pos++] = (char)cur_ch;
}
if (inp_pos >= inp.length || inp[inp_pos] < 0) {
cur_ch = readUTF8Char();
}
else {
cur_ch = inp[inp_pos++];
}
err_buf[err_buf_pos++] = (char)cur_ch;
if (err_buf_pos >= err_buf.length) {
err_buf_pos = 0;
err_buf_cnt++;
}
}
read();
return new String(tmp_buf, 0, tmp_buf_pos);
case '[':
read();
skipWS();
List<Object> l = new ArrayList<Object>();
if (cur_ch <= 0) error();
if (cur_ch != ']') {
for (;;) {
l.add(readNestedObject());
skipWS();
if (cur_ch == ']') break;
if (cur_ch != ',') error();
read();
}
}
read();
return Collections.unmodifiableList(l);
case '{':
read();
skipWS();
Map<String,Object> m = new HashMap<String,Object>();
if (cur_ch <= 0) error();
if (cur_ch != '}') {
for (;;) {
String key = (String)readNestedObject();
skipWS();
if (cur_ch != ':') error();
read();
Object val = readNestedObject();
m.put(key, val);
skipWS();
if (cur_ch == '}') break;
if (cur_ch != ',') error();
read();
}
}
read();
return Collections.unmodifiableMap(m);
case 'n':
read();
if (cur_ch != 'u') error();
read();
if (cur_ch != 'l') error();
read();
if (cur_ch != 'l') error();
read();
return null;
case 'f':
read();
if (cur_ch != 'a') error();
read();
if (cur_ch != 'l') error();
read();
if (cur_ch != 's') error();
read();
if (cur_ch != 'e') error();
read();
return Boolean.FALSE;
case 't':
read();
if (cur_ch != 'r') error();
read();
if (cur_ch != 'u') error();
read();
if (cur_ch != 'e') error();
read();
return Boolean.TRUE;
case 'N':
read();
if (cur_ch != 'a') error();
read();
if (cur_ch != 'N') error();
read();
return Float.NaN;
default:
boolean neg = cur_ch == '-';
if (neg) read();
if (cur_ch >= '0' && cur_ch <= '9') {
int v = 0;
while (v <= 0x7fffffff / 10 - 1) {
v = v * 10 + (cur_ch - '0');
read();
if (cur_ch < '0' || cur_ch > '9') {
if (cur_ch == '.' || cur_ch == 'E' || cur_ch == 'e') {
return readFloat(neg, BigInteger.valueOf(v));
}
if (neg) v = -v;
return Integer.valueOf(v);
}
}
long vl = v;
while (vl < 0x7fffffffffffffffl / 10 - 1) {
vl = vl * 10 + (cur_ch - '0');
read();
if (cur_ch < '0' || cur_ch > '9') {
if (cur_ch == '.' || cur_ch == 'E' || cur_ch == 'e') {
return readFloat(neg, BigInteger.valueOf(vl));
}
if (neg) vl = -vl;
return Long.valueOf(vl);
}
}
StringBuffer sb = new StringBuffer();
sb.append(vl);
while (true) {
sb.append((char)cur_ch);
read();
if (cur_ch < '0' || cur_ch > '9') {
BigInteger n = new BigInteger(sb.toString());
if (cur_ch == '.' || cur_ch == 'E' || cur_ch == 'e') {
return readFloat(neg, n);
}
if (neg) n = n.negate();
return n;
}
}
}
error();
return null;
}
}
/**
* Write an object into JSON output buffer.
* Clients should not call this method directly, except from ObjectWriter implementation.
* @param o - an object to write
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void writeObject(Object o) throws IOException {
if (o == null) {
write("null");
}
else if (o instanceof Boolean) {
write(o.toString());
}
else if (o instanceof Number) {
write(o.toString());
}
else if (o instanceof String) {
String s = (String)o;
char[] arr = new char[s.length()];
s.getChars(0, arr.length, arr, 0);
writeObject(arr);
}
else if (o instanceof char[]) {
char[] s = (char[])o;
write('"');
int l = s.length;
for (int i = 0; i < l; i++) {
char ch = s[i];
switch (ch) {
case 0:
write("\\u0000");
break;
case 1:
write("\\u0001");
break;
case '\r':
write("\\r");
break;
case '\n':
write("\\n");
break;
case '\t':
write("\\t");
break;
case '\b':
write("\\b");
break;
case '\f':
write("\\f");
break;
case '"':
case '\\':
write('\\');
default:
if (tmp_buf_pos >= tmp_buf.length) write(ch);
else tmp_buf[tmp_buf_pos++] = ch;
}
}
write('"');
}
else if (o instanceof Binary) {
Binary b = (Binary)o;
if (zero_copy) {
write('(');
write(Integer.toString(b.size));
write(')');
write((char)1);
bin_buf[bin_buf_pos++] = b;
}
else {
writeObject(Base64.toBase64(b.bytes, b.offs, b.size));
}
}
else if (o instanceof byte[]) {
write('[');
byte[] arr = (byte[])o;
boolean comma = false;
for (int i = 0; i < arr.length; i++) {
if (comma) write(',');
writeUInt(arr[i] & 0xff);
comma = true;
}
write(']');
}
else if (o instanceof Object[]) {
write('[');
Object[] arr = (Object[])o;
boolean comma = false;
for (int i = 0; i < arr.length; i++) {
if (comma) write(',');
writeObject(arr[i]);
comma = true;
}
write(']');
}
else if (o instanceof Collection) {
write('[');
boolean comma = false;
for (Iterator<Object> i = ((Collection<Object>)o).iterator(); i.hasNext();) {
if (comma) write(',');
writeObject(i.next());
comma = true;
}
write(']');
}
else if (o instanceof Map) {
Map<String,Object> map = (Map<String,Object>)o;
write('{');
boolean comma = false;
for (Iterator<Map.Entry<String,Object>> i = map.entrySet().iterator(); i.hasNext();) {
if (comma) write(',');
Map.Entry<String,Object> e = i.next();
writeObject(e.getKey());
write(':');
writeObject(e.getValue());
comma = true;
}
write('}');
}
else {
ObjectWriter writer = object_writers.get(o.getClass());
if (writer == null) {
for (Class<?> c : object_writers.keySet()) {
if (c.isInstance(o)) {
writer = object_writers.get(c);
break;
}
}
}
if (writer != null) {
writer.write(o);
}
else {
throw new IOException("JSON: unsupported object type:" + o.getClass());
}
}
}
private static byte[] toBytes() {
int inp_pos = 0;
int out_pos = 0;
int blc_pos = 0;
while (inp_pos < tmp_buf_pos) {
if (out_pos > tmp_bbf.length - 4) {
byte[] tmp = new byte[tmp_bbf.length * 2];
System.arraycopy(tmp_bbf, 0, tmp, 0, out_pos);
tmp_bbf = tmp;
}
int ch = tmp_buf[inp_pos++];
if (ch == 1) {
Binary b = bin_buf[blc_pos++];
while (out_pos > tmp_bbf.length - b.size) {
byte[] tmp = new byte[tmp_bbf.length * 2];
System.arraycopy(tmp_bbf, 0, tmp, 0, out_pos);
tmp_bbf = tmp;
}
System.arraycopy(b.bytes, b.offs, tmp_bbf, out_pos, b.size);
out_pos += b.size;
}
else if (ch < 0x80) {
tmp_bbf[out_pos++] = (byte)ch;
}
else if (ch < 0x800) {
tmp_bbf[out_pos++] = (byte)((ch >> 6) | 0xc0);
tmp_bbf[out_pos++] = (byte)(ch & 0x3f | 0x80);
}
else if (ch < 0x10000) {
tmp_bbf[out_pos++] = (byte)((ch >> 12) | 0xe0);
tmp_bbf[out_pos++] = (byte)((ch >> 6) & 0x3f | 0x80);
tmp_bbf[out_pos++] = (byte)(ch & 0x3f | 0x80);
}
else {
tmp_bbf[out_pos++] = (byte)((ch >> 18) | 0xf0);
tmp_bbf[out_pos++] = (byte)((ch >> 12) & 0x3f | 0x80);
tmp_bbf[out_pos++] = (byte)((ch >> 6) & 0x3f | 0x80);
tmp_bbf[out_pos++] = (byte)(ch & 0x3f | 0x80);
}
}
byte[] res = new byte[out_pos];
System.arraycopy(tmp_bbf, 0, res, 0, out_pos);
return res;
}
/**
* Convert Java object to JSON string.
* @param o - a Java object
* @return JASON string
* @throws IOException
*/
public static String toJSON(Object o) throws IOException {
assert Protocol.isDispatchThread();
tmp_buf_pos = 0;
bin_buf_pos = 0;
zero_copy = false;
writeObject(o);
return new String(tmp_buf, 0, tmp_buf_pos);
}
/**
* Convert Java object to array of bytes that contains UTF-8 encoded JSON string.
* @param o - a Java object
* @return array of bytes
* @throws IOException
*/
public static byte[] toJASONBytes(Object o) throws IOException {
assert Protocol.isDispatchThread();
tmp_buf_pos = 0;
bin_buf_pos = 0;
zero_copy = false;
writeObject(o);
return toBytes();
}
/**
* Convert multiple Java object to array of bytes that contains
* a sequence of zero terminate UTF-8 encoded JSON strings.
* @param o - array of Java objects
* @return array of bytes
* @throws IOException
*/
public static byte[] toJSONSequence(Object[] o) throws IOException {
assert Protocol.isDispatchThread();
if (o == null || o.length == 0) return null;
tmp_buf_pos = 0;
bin_buf_pos = 0;
zero_copy = false;
for (int i = 0; i < o.length; i++) {
writeObject(o[i]);
write((char)0);
}
return toBytes();
}
/**
* Convert multiple Java object to array of bytes that contains
* a sequence of zero terminate UTF-8 encoded JSON strings.
* @param o - array of Java objects
* @param zero_copy - true to enable "zero copy" JSON extension.
* "zero copy" extension allows insertion of binary data arrays into JSON string.
* The extension allows more efficient transfer of large binary blocs.
* @return array of bytes
* @throws IOException
*/
public static byte[] toJSONSequence(Object[] o, boolean zero_copy) throws IOException {
assert Protocol.isDispatchThread();
if (o == null || o.length == 0) return null;
tmp_buf_pos = 0;
bin_buf_pos = 0;
JSON.zero_copy = zero_copy;
for (int i = 0; i < o.length; i++) {
writeObject(o[i]);
write((char)0);
}
return toBytes();
}
/**
* Convert byte array that contains UTF-8 encoded JSON string to Java object.
* @param b - array of bytes with UTF-8 encoded JSON string
* @return Java object that represents data in the JSON string
* @throws IOException
*/
public static Object parseOne(byte[] b) throws IOException {
assert Protocol.isDispatchThread();
if (b.length == 0) return null;
inp = b;
inp_pos = 0;
err_buf_pos = 0;
err_buf_cnt = 0;
read();
Object o = readNestedObject();
if (cur_ch >= 0) error();
return o;
}
/**
* Convert byte array that contains sequence of zero terminated UTF-8 encoded JSON string
* to array of Java objects.
* @param b - array of bytes with sequence of zero terminated UTF-8 encoded JSON string
* @return array of Java objects that represents data in the sequence of JSON strings
* @throws IOException
*/
public static Object[] parseSequence(byte[] b) throws IOException {
assert Protocol.isDispatchThread();
inp = b;
inp_pos = 0;
err_buf_pos = 0;
err_buf_cnt = 0;
read();
List<Object> l = new ArrayList<Object>();
while (cur_ch >= 0) {
if (cur_ch == 0) l.add(null);
else l.add(readNestedObject());
if (cur_ch != 0) error("missing \\0 terminator");
read();
}
return l.toArray();
}
/**
* Converts a Java object to array of bytes.
* The object is expected to be created from a JSON string by using one of methods in this class.
* If the object is not one of supported representations of binary data, the method throws Error.
* @param o - a Java object representing JSON binary data
* @return array of bytes
*/
public static byte[] toByteArray(Object o) {
if (o == null) return null;
if (o instanceof byte[]) return (byte[])o;
if (o instanceof char[]) return Base64.toByteArray((char[])o);
if (o instanceof String) return Base64.toByteArray(((String)o).toCharArray());
throw new Error();
}
/**
* Converts a Java object to bytes in a given array of bytes.
* The object is expected to be created from a JSON string by using one of methods in this class.
* If the object is not one of supported representations of binary data, the method throws Error.
* @param buf - destination array of bytes
* @param offs - starting position in the destination array
* @param size - the number of bytes to be copied into the destination array
* @param o - a Java object representing JSON binary data
*/
public static void toByteArray(byte[] buf, int offs, int size, Object o) {
if (o instanceof char[]) Base64.toByteArray(buf, offs, size, (char[])o);
else if (o instanceof String) Base64.toByteArray(buf, offs, size, ((String)o).toCharArray());
else if (o != null) System.arraycopy(toByteArray(o), 0, buf, offs, size);
}
/**
* Converts a JSON number to BigInteger.
* @param n - a Number.
* @return BigInteger.
*/
public static BigInteger toBigInteger(Number n) {
if (n == null) return null;
if (n instanceof BigInteger) return (BigInteger)n;
return BigInteger.valueOf(n.longValue());
}
}