// ================================================================================================= // Copyright 2011 Twitter, Inc. // ------------------------------------------------------------------------------------------------- // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this work except in compliance with the License. // You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.thrift.text; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.nio.ByteBuffer; import java.util.Stack; import java.util.logging.Logger; import com.google.common.base.Charsets; import com.google.common.io.ByteStreams; import com.google.gson.JsonElement; import com.google.gson.JsonStreamParser; import com.google.gson.stream.JsonWriter; import org.apache.commons.codec.binary.Base64; import org.apache.thrift.TException; import org.apache.thrift.protocol.TField; import org.apache.thrift.protocol.TList; import org.apache.thrift.protocol.TMap; import org.apache.thrift.protocol.TMessage; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.protocol.TProtocolFactory; import org.apache.thrift.protocol.TSet; import org.apache.thrift.protocol.TStruct; import org.apache.thrift.protocol.TType; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; /** * A simple text format for serializing/deserializing thrift * messages. This format is inefficient in space. * * For an example, see: * tests/resources/com/twitter/common/thrift/text/TTextProtocol_TestData.txt * * which is a text encoding of the thrift message defined in: * * src/main/thrift/com/twitter/common/thrift/text/TTextProtocolTest.thrift * * Whitespace (including newlines) is not significant. * * No comments are allowed in the json. * * We support parsing structs and anything embedded in a struct, * but not messages (which are generated as part of thrift RPC * service definitions). * * TODO(Alex Roetter): write a wrapper that allows us to read in a file * of many structs (perhaps stored in a JsonArray), passing each struct to * this class for parsing. * * See thrift's @see org.apache.thrift.protocol.TJSONProtocol * for another example an implementation of the @see TProtocol * interface. This class is based on that. * * TODO(Alex Roetter): Also add a new TEXT_PROTOCOL field to ThriftCodec * * TODO(Alex Roetter): throw this up on my github. Seems generally useful. * * TODO(Alex Roetter): add support for enums * * @author Alex Roetter */ public class TTextProtocol extends TProtocol { private static final Logger LOG = Logger.getLogger( TTextProtocol.class.getName()); private static final String OBJECT_AS_KEY_ILLEGAL = "A JsonObject (map or struct) can't be a key in a map!"; private static final String SEQUENCE_AS_KEY_ILLEGAL = "Can't have a sequence (list or set) as a key in a map!"; private static final TStruct ANONYMOUS_STRUCT = new TStruct(); // how many bytes to read at once private static final int READ_BUFFER_SIZE = 1024; private static final byte UNUSED_TYPE = TType.STOP; private Stack<BaseContext> contextStack; private Base64 base64Encoder = new Base64(); private JsonWriter writer; private JsonStreamParser parser; /** * Factory */ public static class Factory implements TProtocolFactory { @Override public TProtocol getProtocol(TTransport trans) { return new TTextProtocol(trans); } } /** * Create a parser which can read from trans, and create the output writer * that can write to a TTransport */ public TTextProtocol(TTransport trans) { super(trans); ByteArrayOutputStream mybaos = new TTransportOutputStream(); OutputStreamWriter osw; osw = new OutputStreamWriter(mybaos, Charsets.UTF_8); writer = new JsonWriter(osw); writer.setIndent(" "); // two spaces reset(); try { parser = createParser(); } catch (IOException ex) { // This happens when we're created in write mode (i.e. // there is nothing to parse). Calls to read methods will fail. } contextStack = new Stack<BaseContext>(); contextStack.push(new BaseContext()); } @Override public final void reset() { } /** * I believe these two messages are called for a thrift service * interface. We don't plan on storing any text objects of that * type on disk. */ @Override public void writeMessageBegin(TMessage message) throws TException { unsupportedOperation(); } @Override public void writeMessageEnd() throws TException { unsupportedOperation(); } /** * Throws an exception for invoked operations that we don't support. */ private void unsupportedOperation() { throw new UnsupportedOperationException("Not supported yet."); } @Override public void writeStructBegin(TStruct struct) throws TException { writeJsonObjectBegin(new StructContext(null)); } @Override public void writeStructEnd() throws TException { writeJsonObjectEnd(); } @Override public void writeFieldBegin(TField field) throws TException { try { writer.name(field.name); } catch (IOException ex) { throw new TException(ex); } } @Override public void writeFieldEnd() throws TException { } @Override public void writeFieldStop() throws TException { } @Override public void writeMapBegin(TMap map) throws TException { writeJsonObjectBegin(new MapContext(null)); } @Override public void writeMapEnd() throws TException { writeJsonObjectEnd(); } /** * Helper to write out the beginning of a Thrift type (either struct or map), * both of which are written as JsonObjects. */ private void writeJsonObjectBegin(BaseContext context) throws TException { getCurrentContext().write(); if (getCurrentContext().isMapKey()) { throw new TException(OBJECT_AS_KEY_ILLEGAL); } pushContext(context); try { writer.beginObject(); } catch (IOException ex) { throw new TException(ex); } } /** * Helper to write out the end of a Thrift type (either struct or map), * both of which are written as JsonObjects. * @throws TException */ private void writeJsonObjectEnd() throws TException { try { writer.endObject(); popContext(); // flush at the end of the final struct. if (1 == contextStack.size()) { writer.flush(); } } catch (IOException ex) { throw new TException(ex); } } @Override public void writeListBegin(TList list) throws TException { writeSequenceBegin(list.size); } @Override public void writeListEnd() throws TException { writeSequenceEnd(); } @Override public void writeSetBegin(TSet set) throws TException { writeSequenceBegin(set.size); } @Override public void writeSetEnd() throws TException { writeListEnd(); } /** * Helper shared by write{List/Set}Begin */ private void writeSequenceBegin(int size) throws TException { getCurrentContext().write(); if (getCurrentContext().isMapKey()) { throw new TException(SEQUENCE_AS_KEY_ILLEGAL); } pushContext(new SequenceContext(null)); try { writer.beginArray(); } catch (IOException ex) { throw new TTransportException(ex); } } /** * Helper shared by write{List/Set}End * @throws TException */ private void writeSequenceEnd() throws TException { try { writer.endArray(); } catch (IOException ex) { throw new TTransportException(ex); } popContext(); } @Override public void writeBool(boolean b) throws TException { writeNameOrValue(TypedParser.BOOLEAN, b); } @Override public void writeByte(byte b) throws TException { writeNameOrValue(TypedParser.BYTE, b); } @Override public void writeI16(short i16) throws TException { writeNameOrValue(TypedParser.SHORT, i16); } @Override public void writeI32(int i32) throws TException { writeNameOrValue(TypedParser.INTEGER, i32); } @Override public void writeI64(long i64) throws TException { writeNameOrValue(TypedParser.LONG, i64); } @Override public void writeDouble(double dub) throws TException { writeNameOrValue(TypedParser.DOUBLE, dub); } @Override public void writeString(String str) throws TException { writeNameOrValue(TypedParser.STRING, str); } @Override public void writeBinary(ByteBuffer buf) throws TException { writeString(new String(base64Encoder.encode(buf.array()))); } /** * Write out the given value, either as a JSON name (meaning it's * escaped by quotes), or a value. The TypedParser knows how to * handle the writing. */ private <T> void writeNameOrValue(TypedParser<T> helper, T val) throws TException { getCurrentContext().write(); try { if (getCurrentContext().isMapKey()) { writer.name(val.toString()); } else { helper.writeValue(writer, val); } } catch (IOException ex) { throw new TException(ex); } } ///////////////////////////////////////// // Read methods ///////////////////////////////////////// @Override public TMessage readMessageBegin() throws TException { unsupportedOperation(); return null; } @Override public void readMessageEnd() throws TException { unsupportedOperation(); } @Override public TStruct readStructBegin() throws TException { getCurrentContext().read(); if (getCurrentContext().isMapKey()) { throw new TException(OBJECT_AS_KEY_ILLEGAL); } JsonElement structElem; // Reading a new top level struct if the only item on the stack // is the BaseContext if (1 == contextStack.size()) { structElem = parser.next(); if (null == structElem) { throw new TException("parser.next() has nothing to parse!"); } } else { structElem = getCurrentContext().getCurrentChild(); } if (!structElem.isJsonObject()) { throw new TException("Expected Json Object!"); } pushContext(new StructContext(structElem.getAsJsonObject())); return ANONYMOUS_STRUCT; } @Override public void readStructEnd() throws TException { popContext(); } @Override public TField readFieldBegin() throws TException { String name = null; if (!getCurrentContext().hasMoreChildren()) { return new TField("", UNUSED_TYPE, (short) 0); } getCurrentContext().read(); JsonElement jsonName = getCurrentContext().getCurrentChild(); if (!jsonName.getAsJsonPrimitive().isString()) { throw new RuntimeException("Expected String for a field name"); } return getCurrentContext().getTFieldByName( jsonName.getAsJsonPrimitive().getAsString()); } @Override public void readFieldEnd() throws TException { } @Override public TMap readMapBegin() throws TException { getCurrentContext().read(); if (getCurrentContext().isMapKey()) { throw new TException(OBJECT_AS_KEY_ILLEGAL); } JsonElement curElem = getCurrentContext().getCurrentChild(); if (!curElem.isJsonObject()) { throw new TException("Expected JSON Object!"); } pushContext(new MapContext(curElem.getAsJsonObject())); return new TMap(UNUSED_TYPE, UNUSED_TYPE, curElem.getAsJsonObject().entrySet().size()); } @Override public void readMapEnd() throws TException { popContext(); } @Override public TList readListBegin() throws TException { int size = readSequenceBegin(); return new TList(UNUSED_TYPE, size); } @Override public void readListEnd() throws TException { readSequenceEnd(); } @Override public TSet readSetBegin() throws TException { int size = readSequenceBegin(); return new TSet(UNUSED_TYPE, size); } @Override public void readSetEnd() throws TException { readSequenceEnd(); } /** * Helper shared by read{List/Set}Begin * @return * @throws TException */ private int readSequenceBegin() throws TException { getCurrentContext().read(); if (getCurrentContext().isMapKey()) { throw new TException(SEQUENCE_AS_KEY_ILLEGAL); } JsonElement curElem = getCurrentContext().getCurrentChild(); if (!curElem.isJsonArray()) { throw new TException("Expected JSON Array!"); } pushContext(new SequenceContext(curElem.getAsJsonArray())); return curElem.getAsJsonArray().size(); } /** * Helper shared by read{List/Set}End */ private void readSequenceEnd() { popContext(); } @Override public boolean readBool() throws TException { return readNameOrValue(TypedParser.BOOLEAN); } @Override public byte readByte() throws TException { return readNameOrValue(TypedParser.BYTE); } @Override public short readI16() throws TException { return readNameOrValue(TypedParser.SHORT); } @Override public int readI32() throws TException { return readNameOrValue(TypedParser.INTEGER); } @Override public long readI64() throws TException { return readNameOrValue(TypedParser.LONG); } @Override public double readDouble() throws TException { return readNameOrValue(TypedParser.DOUBLE); } @Override public String readString() throws TException { return readNameOrValue(TypedParser.STRING); } @Override public ByteBuffer readBinary() throws TException { return ByteBuffer.wrap(base64Encoder.decode(readString())); } /** * Read in a value of the given type, either as a name (meaning the * JSONElement is a string and we convert it), or as a value * (meaning the JSONElement has the type we expect). * Uses a TypedParser to do the real work. * * TODO(Alex Roetter): not sure TypedParser is a win for the number of * lines it saves. Consider expanding out all the readX() methods to * do what readNameOrValue does, calling the relevant methods from * the TypedParser directly. */ private <T> T readNameOrValue(TypedParser<T> ch) { getCurrentContext().read(); JsonElement elem = getCurrentContext().getCurrentChild(); if (getCurrentContext().isMapKey()) { // Will throw a ClassCastException if this is not a JsonPrimitve string return ch.readFromString(elem.getAsString()); } else { return ch.readFromJsonElement(elem); } } /** * Set up the stream parser to read from the trans_ TTransport * buffer. */ private JsonStreamParser createParser() throws IOException { return new JsonStreamParser( new String(ByteStreams.toByteArray(new InputStream() { private int index; private int max; private final byte[] buffer = new byte[READ_BUFFER_SIZE]; @Override public int read() throws IOException { if (max == -1) { return -1; } if (max > 0 && index < max) { return buffer[index++]; } try { max = trans_.read(buffer, 0, READ_BUFFER_SIZE); index = 0; } catch (TTransportException e) { if (TTransportException.END_OF_FILE != e.getType()) { throw new IOException(e); } max = -1; } return read(); } }), Charsets.UTF_8)); } /** * Return the current parsing context */ private BaseContext getCurrentContext() { return contextStack.peek(); } /** * Add a new parsing context onto the parse context stack */ private void pushContext(BaseContext c) { contextStack.push(c); } /** * Pop a parsing context from the parse context stack */ private void popContext() { contextStack.pop(); } /** Just a byte array output stream that forwards all data to * a TTransport when it is flushed or closed */ private class TTransportOutputStream extends ByteArrayOutputStream { // This isn't necessary, but a good idea to close the tranport @Override public void close() throws IOException { flush(); super.close(); trans_.close(); } @Override public void flush() throws IOException { try { super.flush(); byte[] bytes = toByteArray(); trans_.write(bytes); trans_.flush(); } catch (TTransportException ex) { throw new IOException(ex); } // Clears the internal memory buffer, since we've already // written it out. super.reset(); } } }