Add Java JSON protocol implementation.

Implement full-featured JSON protocol, low-level base-64 encode/decode
methods, and related tests.

Conflicts (resolved by dreiss):

	test/java/build.xml


git-svn-id: https://svn.apache.org/repos/asf/incubator/thrift/trunk@665562 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/lib/java/src/protocol/TBase64Utils.java b/lib/java/src/protocol/TBase64Utils.java
new file mode 100644
index 0000000..f3fc1c3
--- /dev/null
+++ b/lib/java/src/protocol/TBase64Utils.java
@@ -0,0 +1,116 @@
+// Copyright (c) 2006- Facebook
+// Distributed under the Thrift Software License
+//
+// See accompanying file LICENSE or visit the Thrift site at:
+// http://developers.facebook.com/thrift/
+
+package com.facebook.thrift.protocol;
+
+/**
+ * Class for encoding and decoding Base64 data.
+ *
+ * This class is kept at package level because the interface does no input
+ * validation and is therefore too low-level for generalized reuse.
+ *
+ * Note also that the encoding does not pad with equal signs , as discussed in
+ * section 2.2 of the RFC (http://www.faqs.org/rfcs/rfc3548.html). Furthermore,
+ * bad data encountered when decoding is neither rejected or ignored but simply
+ * results in bad decoded data -- this is not in compliance with the RFC but is
+ * done in the interest of performance.
+ *
+ * @author Chad Walters <chad@powerset.com>
+ */
+class TBase64Utils {
+
+  private static final String ENCODE_TABLE =
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+  /**
+   * Encode len bytes of data in src at offset srcOff, storing the result into
+   * dst at offset dstOff. len must be 1, 2, or 3. dst must have at least len+1
+   * bytes of space at dstOff. src and dst should not be the same object. This
+   * method does no validation of the input values in the interest of
+   * performance.
+   *
+   * @param src  the source of bytes to encode
+   * @param srcOff  the offset into the source to read the unencoded bytes
+   * @param len  the number of bytes to encode (must be 1, 2, or 3).
+   * @param dst  the destination for the encoding
+   * @param dstOff  the offset into the destination to place the encoded bytes
+   */
+  static final void encode(byte[] src, int srcOff, int len,  byte[] dst,
+                           int dstOff) {
+    dst[dstOff] = (byte)ENCODE_TABLE.charAt((src[srcOff] >> 2) & 0x3F);
+    if (len == 3) {
+      dst[dstOff + 1] =
+        (byte)ENCODE_TABLE.charAt(
+                         ((src[srcOff] << 4) + (src[srcOff+1] >> 4)) & 0x3F);
+      dst[dstOff + 2] =
+        (byte)ENCODE_TABLE.charAt(
+                         ((src[srcOff+1] << 2) + (src[srcOff+2] >> 6)) & 0x3F);
+      dst[dstOff + 3] =
+        (byte)ENCODE_TABLE.charAt(src[srcOff+2] & 0x3F);
+    }
+    else if (len == 2) {
+      dst[dstOff+1] =
+        (byte)ENCODE_TABLE.charAt(
+                          ((src[srcOff] << 4) + (src[srcOff+1] >> 4)) & 0x3F);
+      dst[dstOff + 2] =
+        (byte)ENCODE_TABLE.charAt((src[srcOff+1] << 2) & 0x3F);
+
+    }
+    else { // len == 1) {
+      dst[dstOff + 1] =
+        (byte)ENCODE_TABLE.charAt((src[srcOff] << 4) & 0x3F);
+    }
+  }
+
+  private static final byte[] DECODE_TABLE = {
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
+    52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
+    -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
+    15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
+    -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
+    41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+    -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
+  };
+
+  /**
+   * Decode len bytes of data in src at offset srcOff, storing the result into
+   * dst at offset dstOff. len must be 2, 3, or 4. dst must have at least len-1
+   * bytes of space at dstOff. src and dst may be the same object as long as
+   * dstoff <= srcOff. This method does no validation of the input values in
+   * the interest of performance.
+   *
+   * @param src  the source of bytes to decode
+   * @param srcOff  the offset into the source to read the encoded bytes
+   * @param len  the number of bytes to decode (must be 2, 3, or 4)
+   * @param dst  the destination for the decoding
+   * @param dstOff  the offset into the destination to place the decoded bytes
+   */
+  static final void decode(byte[] src, int srcOff, int len,  byte[] dst,
+                           int dstOff) {
+    dst[dstOff] = (byte)
+      ((DECODE_TABLE[src[srcOff] & 0x0FF] << 2) |
+       (DECODE_TABLE[src[srcOff+1] & 0x0FF] >> 4));
+    if (len > 2) {
+      dst[dstOff+1] = (byte)
+        (((DECODE_TABLE[src[srcOff+1] & 0x0FF] << 4) & 0xF0) |
+         (DECODE_TABLE[src[srcOff+2] & 0x0FF] >> 2));
+      if (len > 3) {
+        dst[dstOff+2] = (byte)
+          (((DECODE_TABLE[src[srcOff+2] & 0x0FF] << 6) & 0xC0) |
+           DECODE_TABLE[src[srcOff+3] & 0x0FF]);
+      }
+    }
+  }
+}
diff --git a/lib/java/src/protocol/TJSONProtocol.java b/lib/java/src/protocol/TJSONProtocol.java
new file mode 100644
index 0000000..1e51dd2
--- /dev/null
+++ b/lib/java/src/protocol/TJSONProtocol.java
@@ -0,0 +1,915 @@
+// Copyright (c) 2006- Facebook
+// Distributed under the Thrift Software License
+//
+// See accompanying file LICENSE or visit the Thrift site at:
+// http://developers.facebook.com/thrift/
+
+package com.facebook.thrift.protocol;
+
+import com.facebook.thrift.TException;
+import com.facebook.thrift.TByteArrayOutputStream;
+import com.facebook.thrift.transport.TTransport;
+import java.io.UnsupportedEncodingException;
+import java.util.Stack;
+
+/**
+ * JSON protocol implementation for thrift.
+ *
+ * This is a full-featured protocol supporting write and read.
+ *
+ * Please see the C++ class header for a detailed description of the
+ * protocol's wire format.
+ *
+ * @author Chad Walters <chad@powerset.com>
+ */
+public class TJSONProtocol extends TProtocol {
+
+  /**
+   * Factory for JSON protocol objects
+   */
+  public static class Factory implements TProtocolFactory {
+
+    public TProtocol getProtocol(TTransport trans) {
+      return new TJSONProtocol(trans);
+    }
+
+  }
+
+  private static final byte[] COMMA = new byte[] {','};
+  private static final byte[] COLON = new byte[] {':'};
+  private static final byte[] LBRACE = new byte[] {'{'};
+  private static final byte[] RBRACE = new byte[] {'}'};
+  private static final byte[] LBRACKET = new byte[] {'['};
+  private static final byte[] RBRACKET = new byte[] {']'};
+  private static final byte[] QUOTE = new byte[] {'"'};
+  private static final byte[] BACKSLASH = new byte[] {'\\'};
+  private static final byte[] ZERO = new byte[] {'0'};
+
+  private static final byte[] ESCSEQ = new byte[] {'\\','u','0','0'};
+
+  private static final long  VERSION = 1;
+
+  private static final byte[] JSON_CHAR_TABLE = {
+    /*  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F */
+    0,  0,  0,  0,  0,  0,  0,  0,'b','t','n',  0,'f','r',  0,  0, // 0
+    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 1
+    1,  1,'"',  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1, // 2
+  };
+
+  private static final String ESCAPE_CHARS = "\"\\bfnrt";
+
+  private static final byte[] ESCAPE_CHAR_VALS = {
+    '"', '\\', '\b', '\f', '\n', '\r', '\t',
+  };
+
+  private static final int  DEF_STRING_SIZE = 16;
+
+  private static final byte[] NAME_BOOL = new byte[] {'t', 'f'};
+  private static final byte[] NAME_BYTE = new byte[] {'i','8'};
+  private static final byte[] NAME_I16 = new byte[] {'i','1','6'};
+  private static final byte[] NAME_I32 = new byte[] {'i','3','2'};
+  private static final byte[] NAME_I64 = new byte[] {'i','6','4'};
+  private static final byte[] NAME_DOUBLE = new byte[] {'d','b','l'};
+  private static final byte[] NAME_STRUCT = new byte[] {'r','e','c'};
+  private static final byte[] NAME_STRING = new byte[] {'s','t','r'};
+  private static final byte[] NAME_MAP = new byte[] {'m','a','p'};
+  private static final byte[] NAME_LIST = new byte[] {'l','s','t'};
+  private static final byte[] NAME_SET = new byte[] {'s','e','t'};
+
+  private static final byte[] getTypeNameForTypeID(byte typeID)
+    throws TException {
+    switch (typeID) {
+    case TType.BOOL:
+      return NAME_BOOL;
+    case TType.BYTE:
+      return NAME_BYTE;
+    case TType.I16:
+      return NAME_I16;
+    case TType.I32:
+      return NAME_I32;
+    case TType.I64:
+      return NAME_I64;
+    case TType.DOUBLE:
+      return NAME_DOUBLE;
+    case TType.STRING:
+      return NAME_STRING;
+    case TType.STRUCT:
+      return NAME_STRUCT;
+    case TType.MAP:
+      return NAME_MAP;
+    case TType.SET:
+      return NAME_SET;
+    case TType.LIST:
+      return NAME_LIST;
+    default:
+      throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
+                                   "Unrecognized type");
+    }
+  }
+
+  private static final byte getTypeIDForTypeName(byte[] name)
+    throws TException {
+    byte result = TType.STOP;
+    if (name.length > 1) {
+      switch (name[0]) {
+      case 'd':
+        result = TType.DOUBLE;
+        break;
+      case 'i':
+        switch (name[1]) {
+        case '8':
+          result = TType.BYTE;
+          break;
+        case '1':
+          result = TType.I16;
+          break;
+        case '3':
+          result = TType.I32;
+          break;
+        case '6':
+          result = TType.I64;
+          break;
+        }
+        break;
+      case 'l':
+        result = TType.LIST;
+        break;
+      case 'm':
+        result = TType.MAP;
+        break;
+      case 'r':
+        result = TType.STRUCT;
+        break;
+      case 's':
+        if (name[1] == 't') {
+          result = TType.STRING;
+        }
+        else if (name[1] == 'e') {
+          result = TType.SET;
+        }
+        break;
+      case 't':
+        result = TType.BOOL;
+        break;
+      }
+    }
+    if (result == TType.STOP) {
+      throw new TProtocolException(TProtocolException.NOT_IMPLEMENTED,
+                                   "Unrecognized type");
+    }
+    return result;
+  }
+
+  // Base class for tracking JSON contexts that may require inserting/reading
+  // additional JSON syntax characters
+  // This base context does nothing.
+  protected class JSONBaseContext {
+    protected void write() throws TException {}
+
+    protected void read() throws TException {}
+
+    protected boolean escapeNum() { return false; }
+  }
+
+  // Context for JSON lists. Will insert/read commas before each item except
+  // for the first one
+  protected class JSONListContext extends JSONBaseContext {
+    private boolean first_ = true;
+
+    @Override
+    protected void write() throws TException {
+      if (first_) {
+        first_ = false;
+      } else {
+        trans_.write(COMMA);
+      }
+    }
+
+    @Override
+    protected void read() throws TException {
+      if (first_) {
+        first_ = false;
+      } else {
+        readJSONSyntaxChar(COMMA);
+      }
+    }
+  }
+
+  // Context for JSON records. Will insert/read colons before the value portion
+  // of each record pair, and commas before each key except the first. In
+  // addition, will indicate that numbers in the key position need to be
+  // escaped in quotes (since JSON keys must be strings).
+  protected class JSONPairContext extends JSONBaseContext {
+    private boolean first_ = true;
+    private boolean colon_ = true;
+
+    @Override
+    protected void write() throws TException {
+      if (first_) {
+        first_ = false;
+        colon_ = true;
+      } else {
+        trans_.write(colon_ ? COLON : COMMA);
+        colon_ = !colon_;
+      }
+    }
+
+    @Override
+    protected void read() throws TException {
+      if (first_) {
+        first_ = false;
+        colon_ = true;
+      } else {
+        readJSONSyntaxChar(colon_ ? COLON : COMMA);
+        colon_ = !colon_;
+      }
+    }
+
+    @Override
+    protected boolean escapeNum() {
+      return colon_;
+    }
+  }
+
+  // Holds up to one byte from the transport
+  protected class LookaheadReader {
+
+    private boolean hasData_;
+    private byte[] data_ = new byte[1];
+
+    // Return and consume the next byte to be read, either taking it from the
+    // data buffer if present or getting it from the transport otherwise.
+    protected byte read() throws TException {
+      if (hasData_) {
+        hasData_ = false;
+      }
+      else {
+        trans_.readAll(data_, 0, 1);
+      }
+      return data_[0];
+    }
+
+    // Return the next byte to be read without consuming, filling the data
+    // buffer if it has not been filled already.
+    protected byte peek() throws TException {
+      if (!hasData_) {
+        trans_.readAll(data_, 0, 1);
+      }
+      hasData_ = true;
+      return data_[0];
+    }
+  }
+
+  // Stack of nested contexts that we may be in
+  private Stack<JSONBaseContext> contextStack_ = new Stack<JSONBaseContext>();
+
+  // Current context that we are in
+  private JSONBaseContext context_ = new JSONBaseContext();
+
+  // Reader that manages a 1-byte buffer
+  private LookaheadReader reader_ = new LookaheadReader();
+
+  // Push a new JSON context onto the stack.
+  private void pushContext(JSONBaseContext c) {
+    contextStack_.push(context_);
+    context_ = c;
+  }
+
+  // Pop the last JSON context off the stack
+  private void popContext() {
+    context_ = contextStack_.pop();
+  }
+
+  /**
+   * Constructor
+   */
+  public TJSONProtocol(TTransport trans) {
+    super(trans);
+  }
+
+  // Temporary buffer used by several methods
+  private byte[] tmpbuf_ = new byte[4];
+
+  // Read a byte that must match b[0]; otherwise an excpetion is thrown.
+  // Marked protected to avoid synthetic accessor in JSONListContext.read
+  // and JSONPairContext.read
+  protected void readJSONSyntaxChar(byte[] b) throws TException {
+    byte ch = reader_.read();
+    if (ch != b[0]) {
+      throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                   "Unexpected character:" + (char)ch);
+    }
+  }
+
+  // Convert a byte containing a hex char ('0'-'9' or 'a'-'f') into its
+  // corresponding hex value
+  private static final byte hexVal(byte ch) throws TException {
+    if ((ch >= '0') && (ch <= '9')) {
+      return (byte)((char)ch - '0');
+    }
+    else if ((ch >= 'a') && (ch <= 'f')) {
+      return (byte)((char)ch - 'a');
+    }
+    else {
+      throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                   "Expected hex character");
+    }
+  }
+
+  // Convert a byte containing a hex value to its corresponding hex character
+  private static final byte hexChar(byte val) {
+    val &= 0x0F;
+    if (val < 10) {
+      return (byte)((char)val + '0');
+    }
+    else {
+      return (byte)((char)val + 'a');
+    }
+  }
+
+  // Write the bytes in array buf as a JSON characters, escaping as needed
+  private void writeJSONString(byte[] b) throws TException {
+    context_.write();
+    trans_.write(QUOTE);
+    int len = b.length;
+    for (int i = 0; i < len; i++) {
+      if ((b[i] & 0x00FF) >= 0x30) {
+        if (b[i] == BACKSLASH[0]) {
+          trans_.write(BACKSLASH);
+          trans_.write(BACKSLASH);
+        }
+        else {
+          trans_.write(b, i, 1);
+        }
+      }
+      else {
+        tmpbuf_[0] = JSON_CHAR_TABLE[b[i]];
+        if (tmpbuf_[0] == 1) {
+          trans_.write(b, i, 1);
+        }
+        else if (tmpbuf_[0] > 1) {
+          trans_.write(BACKSLASH);
+          trans_.write(tmpbuf_, 0, 1);
+        }
+        else {
+          trans_.write(ESCSEQ);
+          tmpbuf_[0] = hexChar((byte)(b[i] >> 4));
+          tmpbuf_[1] = hexChar(b[i]);
+          trans_.write(tmpbuf_, 0, 2);
+        }
+      }
+    }
+    trans_.write(QUOTE);
+  }
+
+  // Write out number as a JSON value. If the context dictates so, it will be
+  // wrapped in quotes to output as a JSON string.
+  private void writeJSONInteger(long num) throws TException {
+    context_.write();
+    String str = Long.toString(num);
+    boolean escapeNum = context_.escapeNum();
+    if (escapeNum) {
+      trans_.write(QUOTE);
+    }
+    try {
+      byte[] buf = str.getBytes("UTF-8");
+      trans_.write(buf);
+    } catch (UnsupportedEncodingException uex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+    if (escapeNum) {
+      trans_.write(QUOTE);
+    }
+  }
+
+  // Write out a double as a JSON value. If it is NaN or infinity or if the
+  // context dictates escaping, write out as JSON string.
+  private void writeJSONDouble(double num) throws TException {
+    context_.write();
+    String str = Double.toString(num);
+    boolean special = false;
+    switch (str.charAt(0)) {
+    case 'N': // NaN
+    case 'I': // Infinity
+      special = true;
+      break;
+    case '-':
+      if (str.charAt(1) == 'I') { // -Infinity
+        special = true;
+      }
+      break;
+    }
+
+    boolean escapeNum = special || context_.escapeNum();
+    if (escapeNum) {
+      trans_.write(QUOTE);
+    }
+    try {
+      byte[] b = str.getBytes("UTF-8");
+      trans_.write(b, 0, b.length);
+    } catch (UnsupportedEncodingException uex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+    if (escapeNum) {
+      trans_.write(QUOTE);
+    }
+  }
+
+  // Write out contents of byte array b as a JSON string with base-64 encoded
+  // data
+  private void writeJSONBase64(byte[] b) throws TException {
+    context_.write();
+    trans_.write(QUOTE);
+    int len = b.length;
+    int off = 0;
+    while (len >= 3) {
+      // Encode 3 bytes at a time
+      TBase64Utils.encode(b, off, 3, tmpbuf_, 0);
+      trans_.write(tmpbuf_, 0, 4);
+      off += 3;
+      len -= 3;
+    }
+    if (len > 0) {
+      // Encode remainder
+      TBase64Utils.encode(b, off, len, tmpbuf_, 0);
+      trans_.write(tmpbuf_, 0, len + 1);
+    }
+    trans_.write(QUOTE);
+  }
+
+  private void writeJSONObjectStart() throws TException {
+    context_.write();
+    trans_.write(LBRACE);
+    pushContext(new JSONPairContext());
+  }
+
+  private void writeJSONObjectEnd() throws TException {
+    popContext();
+    trans_.write(RBRACE);
+  }
+
+  private void writeJSONArrayStart() throws TException {
+    context_.write();
+    trans_.write(LBRACKET);
+    pushContext(new JSONListContext());
+  }
+
+  private void writeJSONArrayEnd() throws TException {
+    popContext();
+    trans_.write(RBRACKET);
+  }
+
+  @Override
+  public void writeMessageBegin(TMessage message) throws TException {
+    writeJSONArrayStart();
+    writeJSONInteger(VERSION);
+    try {
+      byte[] b = message.name.getBytes("UTF-8");
+      writeJSONString(b);
+    } catch (UnsupportedEncodingException uex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+    writeJSONInteger(message.type);
+    writeJSONInteger(message.seqid);
+  }
+
+  @Override
+  public void writeMessageEnd() throws TException {
+    writeJSONArrayEnd();
+  }
+
+  @Override
+  public void writeStructBegin(TStruct struct) throws TException {
+    writeJSONObjectStart();
+  }
+
+  @Override
+  public void writeStructEnd() throws TException {
+    writeJSONObjectEnd();
+  }
+
+  @Override
+  public void writeFieldBegin(TField field) throws TException {
+    writeJSONInteger(field.id);
+    writeJSONObjectStart();
+    writeJSONString(getTypeNameForTypeID(field.type));
+  }
+
+  @Override
+  public void writeFieldEnd() throws TException {
+    writeJSONObjectEnd();
+  }
+
+  @Override
+  public void writeFieldStop() {}
+
+  @Override
+  public void writeMapBegin(TMap map) throws TException {
+    writeJSONArrayStart();
+    writeJSONString(getTypeNameForTypeID(map.keyType));
+    writeJSONString(getTypeNameForTypeID(map.valueType));
+    writeJSONInteger(map.size);
+    writeJSONObjectStart();
+  }
+
+  @Override
+  public void writeMapEnd() throws TException {
+    writeJSONObjectEnd();
+    writeJSONArrayEnd();
+  }
+
+  @Override
+  public void writeListBegin(TList list) throws TException {
+    writeJSONArrayStart();
+    writeJSONString(getTypeNameForTypeID(list.elemType));
+    writeJSONInteger(list.size);
+  }
+
+  @Override
+  public void writeListEnd() throws TException {
+    writeJSONArrayEnd();
+  }
+
+  @Override
+  public void writeSetBegin(TSet set) throws TException {
+    writeJSONArrayStart();
+    writeJSONString(getTypeNameForTypeID(set.elemType));
+    writeJSONInteger(set.size);
+  }
+
+  @Override
+  public void writeSetEnd() throws TException {
+    writeJSONArrayEnd();
+  }
+
+  @Override
+  public void writeBool(boolean b) throws TException {
+    writeJSONInteger(b ? (long)1 : (long)0);
+  }
+
+  @Override
+  public void writeByte(byte b) throws TException {
+    writeJSONInteger((long)b);
+  }
+
+  @Override
+  public void writeI16(short i16) throws TException {
+    writeJSONInteger((long)i16);
+  }
+
+  @Override
+  public void writeI32(int i32) throws TException {
+    writeJSONInteger((long)i32);
+  }
+
+  @Override
+  public void writeI64(long i64) throws TException {
+    writeJSONInteger(i64);
+  }
+
+  @Override
+  public void writeDouble(double dub) throws TException {
+    writeJSONDouble(dub);
+  }
+
+  @Override
+  public void writeString(String str) throws TException {
+    try {
+      byte[] b = str.getBytes("UTF-8");
+      writeJSONString(b);
+    } catch (UnsupportedEncodingException uex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+  }
+
+  @Override
+  public void writeBinary(byte[] bin) throws TException {
+    writeJSONBase64(bin);
+  }
+
+  /**
+   * Reading methods.
+   */
+
+  // Read in a JSON string, unescaping as appropriate.. Skip reading from the
+  // context if skipContext is true.
+  private TByteArrayOutputStream readJSONString(boolean skipContext)
+    throws TException {
+    TByteArrayOutputStream arr = new TByteArrayOutputStream(DEF_STRING_SIZE);
+    if (!skipContext) {
+      context_.read();
+    }
+    readJSONSyntaxChar(QUOTE);
+    while (true) {
+      byte ch = reader_.read();
+      if (ch == QUOTE[0]) {
+        break;
+      }
+      if (ch == ESCSEQ[0]) {
+        ch = reader_.read();
+        if (ch == ESCSEQ[1]) {
+          readJSONSyntaxChar(ZERO);
+          readJSONSyntaxChar(ZERO);
+          trans_.readAll(tmpbuf_, 0, 2);
+          ch = (byte)((hexVal((byte)tmpbuf_[0]) << 4) + hexVal(tmpbuf_[1]));
+        }
+        else {
+          int off = ESCAPE_CHARS.indexOf(ch);
+          if (off == -1) {
+            throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                         "Expected control char");
+          }
+          ch = ESCAPE_CHAR_VALS[off];
+        }
+      }
+      arr.write(ch);
+    }
+    return arr;
+  }
+
+  // Return true if the given byte could be a valid part of a JSON number.
+  private boolean isJSONNumeric(byte b) {
+    switch (b) {
+    case '+':
+    case '-':
+    case '.':
+    case '0':
+    case '1':
+    case '2':
+    case '3':
+    case '4':
+    case '5':
+    case '6':
+    case '7':
+    case '8':
+    case '9':
+    case 'E':
+    case 'e':
+      return true;
+    }
+    return false;
+  }
+
+  // Read in a sequence of characters that are all valid in JSON numbers. Does
+  // not do a complete regex check to validate that this is actually a number.
+  private String readJSONNumericChars() throws TException {
+    StringBuilder strbld = new StringBuilder();
+    while (true) {
+      byte ch = reader_.peek();
+      if (!isJSONNumeric(ch)) {
+        break;
+      }
+      strbld.append((char)reader_.read());
+    }
+    return strbld.toString();
+  }
+
+  // Read in a JSON number. If the context dictates, read in enclosing quotes.
+  private long readJSONInteger() throws TException {
+    context_.read();
+    if (context_.escapeNum()) {
+      readJSONSyntaxChar(QUOTE);
+    }
+    String str = readJSONNumericChars();
+    if (context_.escapeNum()) {
+      readJSONSyntaxChar(QUOTE);
+    }
+    try {
+      return Long.valueOf(str);
+    }
+    catch (NumberFormatException ex) {
+      throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                   "Bad data encounted in numeric data");
+    }
+  }
+
+  // Read in a JSON double value. Throw if the value is not wrapped in quotes
+  // when expected or if wrapped in quotes when not expected.
+  private double readJSONDouble() throws TException {
+    context_.read();
+    if (reader_.peek() == QUOTE[0]) {
+      TByteArrayOutputStream arr = readJSONString(true);
+      try {
+        double dub = Double.valueOf(arr.toString("UTF-8"));
+        if (!context_.escapeNum() && !Double.isNaN(dub) &&
+            !Double.isInfinite(dub)) {
+          // Throw exception -- we should not be in a string in this case
+          throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                       "Numeric data unexpectedly quoted");
+        }
+        return dub;
+      }
+      catch (UnsupportedEncodingException ex) {
+        throw new TException("JVM DOES NOT SUPPORT UTF-8");
+      }
+    }
+    else {
+      if (context_.escapeNum()) {
+        // This will throw - we should have had a quote if escapeNum == true
+        readJSONSyntaxChar(QUOTE);
+      }
+      try {
+        return Double.valueOf(readJSONNumericChars());
+      }
+      catch (NumberFormatException ex) {
+        throw new TProtocolException(TProtocolException.INVALID_DATA,
+                                     "Bad data encounted in numeric data");
+      }
+    }
+  }
+
+  // Read in a JSON string containing base-64 encoded data and decode it.
+  private byte[] readJSONBase64() throws TException {
+    TByteArrayOutputStream arr = readJSONString(false);
+    byte[] b = arr.get();
+    int len = arr.len();
+    int off = 0;
+    int size = 0;
+    while (len >= 4) {
+      // Decode 4 bytes at a time
+      TBase64Utils.decode(b, off, 4, b, size); // NB: decoded in place
+      off += 4;
+      len -= 4;
+      size += 3;
+    }
+    // Don't decode if we hit the end or got a single leftover byte (invalid
+    // base64 but legal for skip of regular string type)
+    if (len > 1) {
+      // Decode remainder
+      TBase64Utils.decode(b, off, len, b, size); // NB: decoded in place
+      size += len - 1;
+    }
+    // Sadly we must copy the byte[] (any way around this?)
+    byte [] result = new byte[size];
+    System.arraycopy(b, 0, result, 0, size);
+    return result;
+  }
+
+  private void readJSONObjectStart() throws TException {
+    context_.read();
+    readJSONSyntaxChar(LBRACE);
+    pushContext(new JSONPairContext());
+  }
+
+  private void readJSONObjectEnd() throws TException {
+    readJSONSyntaxChar(RBRACE);
+    popContext();
+  }
+
+  private void readJSONArrayStart() throws TException {
+    context_.read();
+    readJSONSyntaxChar(LBRACKET);
+    pushContext(new JSONListContext());
+  }
+
+  private void readJSONArrayEnd() throws TException {
+    readJSONSyntaxChar(RBRACKET);
+    popContext();
+  }
+
+  @Override
+  public TMessage readMessageBegin() throws TException {
+    TMessage message = new TMessage();
+    readJSONArrayStart();
+    if (readJSONInteger() != VERSION) {
+      throw new TProtocolException(TProtocolException.BAD_VERSION,
+                                   "Message contained bad version.");
+    }
+    try {
+      message.name = readJSONString(false).toString("UTF-8");
+    }
+    catch (UnsupportedEncodingException ex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+    message.type = (byte) readJSONInteger();
+    message.seqid = (int) readJSONInteger();
+    return message;
+  }
+
+  @Override
+  public void readMessageEnd() throws TException {
+    readJSONArrayEnd();
+  }
+
+  @Override
+  public TStruct readStructBegin() throws TException {
+    readJSONObjectStart();
+    return new TStruct();
+  }
+
+  @Override
+  public void readStructEnd() throws TException {
+    readJSONObjectEnd();
+  }
+
+  @Override
+  public TField readFieldBegin() throws TException {
+    TField field = new TField();
+    byte ch = reader_.peek();
+    if (ch == RBRACE[0]) {
+      field.type = TType.STOP;
+    }
+    else {
+      field.id = (short) readJSONInteger();
+      readJSONObjectStart();
+      field.type = getTypeIDForTypeName(readJSONString(false).get());
+    }
+    return field;
+  }
+
+  @Override
+  public void readFieldEnd() throws TException {
+    readJSONObjectEnd();
+  }
+
+  @Override
+  public TMap readMapBegin() throws TException {
+    TMap map = new TMap();
+    readJSONArrayStart();
+    map.keyType = getTypeIDForTypeName(readJSONString(false).get());
+    map.valueType = getTypeIDForTypeName(readJSONString(false).get());
+    map.size = (int)readJSONInteger();
+    readJSONObjectStart();
+    return map;
+  }
+
+  @Override
+  public void readMapEnd() throws TException {
+    readJSONObjectEnd();
+    readJSONArrayEnd();
+  }
+
+  @Override
+  public TList readListBegin() throws TException {
+    TList list = new TList();
+    readJSONArrayStart();
+    list.elemType = getTypeIDForTypeName(readJSONString(false).get());
+    list.size = (int)readJSONInteger();
+    return list;
+  }
+
+  @Override
+  public void readListEnd() throws TException {
+    readJSONArrayEnd();
+  }
+
+  @Override
+  public TSet readSetBegin() throws TException {
+    TSet set = new TSet();
+    readJSONArrayStart();
+    set.elemType = getTypeIDForTypeName(readJSONString(false).get());
+    set.size = (int)readJSONInteger();
+    return set;
+  }
+
+  @Override
+  public void readSetEnd() throws TException {
+    readJSONArrayEnd();
+  }
+
+  @Override
+  public boolean readBool() throws TException {
+    return (readJSONInteger() == 0 ? false : true);
+  }
+
+  @Override
+  public byte readByte() throws TException {
+    return (byte) readJSONInteger();
+  }
+
+  @Override
+  public short readI16() throws TException {
+    return (short) readJSONInteger();
+  }
+
+  @Override
+  public int readI32() throws TException {
+    return (int) readJSONInteger();
+  }
+
+  @Override
+  public long readI64() throws TException {
+    return (long) readJSONInteger();
+  }
+
+  @Override
+  public double readDouble() throws TException {
+    return readJSONDouble();
+  }
+
+  @Override
+  public String readString() throws TException {
+    try {
+      return readJSONString(false).toString("UTF-8");
+    }
+    catch (UnsupportedEncodingException ex) {
+      throw new TException("JVM DOES NOT SUPPORT UTF-8");
+    }
+  }
+
+  @Override
+  public byte[] readBinary() throws TException {
+    return readJSONBase64();
+  }
+
+}
diff --git a/test/java/build.xml b/test/java/build.xml
index 7eed8c3..77267fc 100644
--- a/test/java/build.xml
+++ b/test/java/build.xml
@@ -6,6 +6,7 @@
   <property name="gen" location="gen-java" />
   <property name="build" location="build" />
   <property name="cpath" location="../../lib/java/libthrift.jar:/usr/share/java/commons-lang-2.3.jar" />
+  <property name="testjar" location="thrifttest.jar" />
 
   <target name="init">
     <tstamp />
@@ -22,6 +23,9 @@
     <exec executable="../../compiler/cpp/thrift">
       <arg line="--gen java:hashcode ../OptionalRequiredTest.thrift" />
     </exec>
+    <exec executable="../../compiler/cpp/thrift">
+      <arg line="--gen java ../DebugProtoTest.thrift" />
+    </exec>
   </target>
 
   <target name="compileonly">
@@ -30,12 +34,16 @@
   </target>
 
   <target name="compile" depends="init,generate,compileonly">
-    <jar jarfile="thrifttest.jar" basedir="${build}"/>
+    <jar jarfile="${testjar}" basedir="${build}"/>
   </target>
 
   <target name="test" depends="compile">
-    <java classname="com.facebook.thrift.test.IdentityTest" classpath="${cpath}:./thrifttest.jar"/>
-    <java classname="com.facebook.thrift.test.EqualityTest" classpath="${cpath}:./thrifttest.jar"/>
+    <java classname="com.facebook.thrift.test.JSONProtoTest"
+      classpath="${cpath}:${testjar}:${gen}" failonerror="true" />
+    <java classname="com.facebook.thrift.test.IdentityTest"
+      classpath="${cpath}:${testjar}:${gen}" failonerror="true" />
+    <java classname="com.facebook.thrift.test.EqualityTest"
+      classpath="${cpath}:${testjar}:${gen}" failonerror="true" />
   </target>
 
   <target name="clean">
diff --git a/test/java/src/JSONProtoTest.java b/test/java/src/JSONProtoTest.java
new file mode 100644
index 0000000..4177765
--- /dev/null
+++ b/test/java/src/JSONProtoTest.java
@@ -0,0 +1,163 @@
+package com.facebook.thrift.test;
+
+// Generated code
+import thrift.test.*;
+
+import com.facebook.thrift.transport.TMemoryBuffer;
+import com.facebook.thrift.protocol.TJSONProtocol;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Tests for the Java implementation of TJSONProtocol. Mirrors the C++ version
+ *
+ * @author Chad Walters <chad@powerset.com>
+ */
+public class JSONProtoTest {
+
+  private static final byte[] kUnicodeBytes = {
+    (byte)0xd3, (byte)0x80, (byte)0xe2, (byte)0x85, (byte)0xae, (byte)0xce,
+    (byte)0x9d, (byte)0x20, (byte)0xd0, (byte)0x9d, (byte)0xce, (byte)0xbf,
+    (byte)0xe2, (byte)0x85, (byte)0xbf, (byte)0xd0, (byte)0xbe, (byte)0xc9,
+    (byte)0xa1, (byte)0xd0, (byte)0xb3, (byte)0xd0, (byte)0xb0, (byte)0xcf,
+    (byte)0x81, (byte)0xe2, (byte)0x84, (byte)0x8e, (byte)0x20, (byte)0xce,
+    (byte)0x91, (byte)0x74, (byte)0x74, (byte)0xce, (byte)0xb1, (byte)0xe2,
+    (byte)0x85, (byte)0xbd, (byte)0xce, (byte)0xba, (byte)0x83, (byte)0xe2,
+    (byte)0x80, (byte)0xbc
+  };
+
+  public static void main(String [] args) throws Exception {
+   try {
+      System.out.println("In JSON Proto test");
+
+      OneOfEach ooe = new OneOfEach();
+      ooe.im_true   = true;
+      ooe.im_false  = false;
+      ooe.a_bite    = (byte)0xd6;
+      ooe.integer16 = 27000;
+      ooe.integer32 = 1<<24;
+      ooe.integer64 = (long)6000 * 1000 * 1000;
+      ooe.double_precision = Math.PI;
+      ooe.some_characters  = "JSON THIS! \"\1";
+      ooe.zomg_unicode     = new String(kUnicodeBytes, "UTF-8");
+
+
+      Nesting n = new Nesting(new Bonk(), new OneOfEach());
+      n.my_ooe.integer16 = 16;
+      n.my_ooe.integer32 = 32;
+      n.my_ooe.integer64 = 64;
+      n.my_ooe.double_precision = (Math.sqrt(5)+1)/2;
+      n.my_ooe.some_characters  = ":R (me going \"rrrr\")";
+      n.my_ooe.zomg_unicode     = new String(kUnicodeBytes, "UTF-8");
+      n.my_bonk.type    = 31337;
+      n.my_bonk.message = "I am a bonk... xor!";
+
+      HolyMoley hm = new HolyMoley();
+
+      hm.big = new ArrayList<OneOfEach>();
+      hm.big.add(ooe);
+      hm.big.add(n.my_ooe);
+      hm.big.get(0).a_bite = (byte)0x22;
+      hm.big.get(1).a_bite = (byte)0x23;
+
+      hm.contain = new HashSet<List<String>>();
+      ArrayList<String> stage1 = new ArrayList<String>(2);
+      stage1.add("and a one");
+      stage1.add("and a two");
+      hm.contain.add(stage1);
+      stage1 = new ArrayList<String>(3);
+      stage1.add("then a one, two");
+      stage1.add("three!");
+      stage1.add("FOUR!!");
+      hm.contain.add(stage1);
+      stage1 = new ArrayList<String>(0);
+      hm.contain.add(stage1);
+
+      ArrayList<Bonk> stage2 = new ArrayList<Bonk>();
+      hm.bonks = new HashMap<String, List<Bonk>>();
+      hm.bonks.put("nothing", stage2);
+      Bonk b = new Bonk();
+      b.type = 1;
+      b.message = "Wait.";
+      stage2.add(b);
+      b = new Bonk();
+      b.type = 2;
+      b.message = "What?";
+      stage2.add(b);
+      stage2 = new ArrayList<Bonk>();
+      hm.bonks.put("something", stage2);
+      b = new Bonk();
+      b.type = 3;
+      b.message = "quoth";
+      b = new Bonk();
+      b.type = 4;
+      b.message = "the raven";
+      b = new Bonk();
+      b.type = 5;
+      b.message = "nevermore";
+      hm.bonks.put("poe", stage2);
+
+      TMemoryBuffer buffer = new TMemoryBuffer(1024);
+      TJSONProtocol proto = new TJSONProtocol(buffer);
+
+      System.out.println("Writing ooe");
+      ooe.write(proto);
+      System.out.println("Reading ooe");
+      OneOfEach ooe2 = new OneOfEach();
+      ooe2.read(proto);
+
+      System.out.println("Comparing ooe");
+      if (!ooe.equals(ooe2)) {
+        throw new RuntimeException("ooe != ooe2");
+      }
+
+      System.out.println("Writing hm");
+      hm.write(proto);
+
+      System.out.println("Reading hm");
+      HolyMoley hm2 = new HolyMoley();
+      hm2.read(proto);
+
+      System.out.println("Comparing hm");
+      if (!hm.equals(hm2)) {
+        throw new RuntimeException("hm != hm2");
+      }
+
+      hm2.big.get(0).a_bite = (byte)0xFF;
+      if (hm.equals(hm2)) {
+        throw new RuntimeException("hm should not equal hm2");
+      }
+
+      Base64 base = new Base64();
+      base.a = 123;
+      base.b1 = "1".getBytes("UTF-8");
+      base.b2 = "12".getBytes("UTF-8");
+      base.b3 = "123".getBytes("UTF-8");
+      base.b4 = "1234".getBytes("UTF-8");
+      base.b5 = "12345".getBytes("UTF-8");
+      base.b6 = "123456".getBytes("UTF-8");
+
+      System.out.println("Writing base");
+      base.write(proto);
+
+      System.out.println("Reading base");
+      Base64 base2 = new Base64();
+      base2.read(proto);
+
+      System.out.println("Comparing base");
+      if (!base.equals(base2)) {
+        throw new RuntimeException("base != base2");
+      }
+
+    } catch (Exception ex) {
+      ex.printStackTrace();
+      throw ex;
+   }
+  }
+
+}