// ================================================================================================= // 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.linecorp.armeria.common.thrift.text; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Stack; import org.apache.thrift.TBase; import org.apache.thrift.TEnum; 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.TMessageType; 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.scheme.IScheme; import org.apache.thrift.scheme.StandardScheme; import org.apache.thrift.transport.TTransport; import org.apache.thrift.transport.TTransportException; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser.Feature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; /** * A simple text format for serializing/deserializing thrift * messages. This format is inefficient in space. * * <p>For an example, see: * tests/resources/com/twitter/common/thrift/text/TTextProtocol_TestData.txt * * <p>which is a text encoding of the thrift message defined in: * * <p>src/main/thrift/com/twitter/common/thrift/text/TTextProtocolTest.thrift * * <p>Whitespace (including newlines) is not significant. * * <p>No comments are allowed in the json. * * <p>Messages must be formatted as a JSON object with a field 'method' containing * the message name, 'type' containing the message type as an uppercase string * corresponding to {@link TMessageType}, 'args' containing a JSON object with * the actual arguments, and an optional 'seqid' field containing the sequence * id. If 'seqid' is not provided, it will be treated as 0. 'args' should use * the argument names as defined in the service definition. * * <p>Example:{@code * * { * "method": "GetItem", * "type": "CALL", * "args": { * "id": 1, * "fetchAll": true * }, * "seqid": 100 * } * * } * * <p>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. * * <p>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. * * <p>TODO(Alex Roetter): Also add a new TEXT_PROTOCOL field to ThriftCodec * * <p>TODO: Support map enum keys specified as strings. * * <p>TODO: Support string values for enums that have been typedef'd. */ public class TTextProtocol extends TProtocol { 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 ObjectMapper OBJECT_MAPPER = new ObjectMapper() .configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); 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 final Stack<WriterByteArrayOutputStream> writers; private final Stack<BaseContext> contextStack; private final Stack<Class<?>> currentFieldClass; private JsonNode root; /** * 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); writers = new Stack<>(); contextStack = new Stack<>(); currentFieldClass = new Stack<>(); reset(); } @Override @SuppressWarnings("rawtypes") public Class<? extends IScheme> getScheme() { return StandardScheme.class; } @Override public final void reset() { root = null; writers.clear(); pushWriter(new TTransportOutputStream()); contextStack.clear(); contextStack.push(new BaseContext()); currentFieldClass.clear(); } /** * 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 { try { getCurrentWriter().writeStartObject(); getCurrentWriter().writeFieldName("method"); getCurrentWriter().writeString(message.name); getCurrentWriter().writeFieldName("type"); TypedParser.TMESSAGE_TYPE.writeValue(getCurrentWriter(), message.type); getCurrentWriter().writeFieldName("seqid"); getCurrentWriter().writeNumber(message.seqid); getCurrentWriter().writeFieldName("args"); } catch (IOException e) { throw new TTransportException(e); } } @Override public void writeMessageEnd() throws TException { try { getCurrentWriter().writeEndObject(); getCurrentWriter().flush(); } catch (IOException e) { throw new TTransportException(e); } } @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 { getCurrentWriter().writeFieldName(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()) { pushWriter(new ByteArrayOutputStream()); } pushContext(context); try { getCurrentWriter().writeStartObject(); } 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. */ private void writeJsonObjectEnd() throws TException { try { getCurrentWriter().writeEndObject(); popContext(); if (getCurrentContext().isMapKey()) { String writerString = getWriterString(); popWriter(); getCurrentWriter().writeFieldName(writerString); } // flush at the end of the final struct. if (1 == contextStack.size()) { getCurrentWriter().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 { getCurrentWriter().writeStartArray(); } catch (IOException ex) { throw new TTransportException(ex); } } /** * Helper shared by write{List/Set}End. */ private void writeSequenceEnd() throws TException { try { getCurrentWriter().writeEndArray(); } 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 { writeNameOrValue(TypedParser.BINARY, buf); } /** * 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()) { getCurrentWriter().writeFieldName(val.toString()); } else { helper.writeValue(getCurrentWriter(), val); } } catch (IOException ex) { throw new TException(ex); } } ///////////////////////////////////////// // Read methods ///////////////////////////////////////// @Override public TMessage readMessageBegin() throws TException { root = null; try { readRoot(); } catch (IOException e) { throw new TException("Could not parse input, is it valid json?", e); } if (!root.isObject()) { throw new TException("The top level of the input must be a json object with method and args!"); } if (!root.has("method")) { throw new TException("Object must have field 'method' with the rpc method name!"); } String methodName = root.get("method").asText(); if (!root.has("type")) { throw new TException( "Object must have field 'type' with the message type (CALL, REPLY, EXCEPTION, ONEWAY)!"); } Byte messageType = TypedParser.TMESSAGE_TYPE.readFromJsonElement(root.get("type")); if (!root.has("args") || !root.get("args").isObject()) { throw new TException("Object must have field 'args' with the rpc method args!"); } int sequenceId = root.has("seqid") ? root.get("seqid").asInt() : 0; // Override the root with the content of args - thrift's rpc reading will // proceed to read it as a message object. root = root.get("args"); return new TMessage(methodName, messageType, sequenceId); } @Override public void readMessageEnd() throws TException { // We've already finished parsing the top level struct in // readMessageBegin, so nothing to do here. root = null; } @Override public TStruct readStructBegin() throws TException { getCurrentContext().read(); JsonNode structElem; // Reading a new top level struct if the only item on the stack // is the BaseContext if (1 == contextStack.size()) { try { readRoot(); } catch (IOException e) { throw new TException("Could not parse input, is it valid json?", e); } if (root == null) { throw new TException("parser.next() has nothing to parse!"); } structElem = root; } else { structElem = getCurrentContext().getCurrentChild(); } if (getCurrentContext().isMapKey()) { try { structElem = OBJECT_MAPPER.readTree(structElem.asText()); } catch (IOException e) { throw new TException("Could not parse map key, is it valid json?", e); } } if (!structElem.isObject()) { throw new TException("Expected Json Object!"); } Class<?> fieldClass = getCurrentFieldClassIfIs(TBase.class); if (fieldClass != null) { pushContext(new StructContext(structElem, fieldClass)); } else { pushContext(new StructContext(structElem)); } return ANONYMOUS_STRUCT; } @Override public void readStructEnd() throws TException { popContext(); } @Override public TField readFieldBegin() throws TException { if (!getCurrentContext().hasMoreChildren()) { return new TField("", UNUSED_TYPE, (short) 0); } getCurrentContext().read(); JsonNode jsonName = getCurrentContext().getCurrentChild(); if (!jsonName.isTextual()) { throw new RuntimeException("Expected String for a field name"); } String fieldName = jsonName.asText(); currentFieldClass.push(getCurrentContext().getClassByFieldName(fieldName)); return getCurrentContext().getTFieldByName(fieldName); } @Override public void readFieldEnd() throws TException { currentFieldClass.pop(); } @Override public TMap readMapBegin() throws TException { getCurrentContext().read(); JsonNode curElem = getCurrentContext().getCurrentChild(); if (getCurrentContext().isMapKey()) { try { curElem = OBJECT_MAPPER.readTree(curElem.asText()); } catch (IOException e) { throw new TException("Could not parse map key, is it valid json?", e); } } if (!curElem.isObject()) { throw new TException("Expected JSON Object!"); } pushContext(new MapContext(curElem)); return new TMap(UNUSED_TYPE, UNUSED_TYPE, curElem.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. */ private int readSequenceBegin() throws TException { getCurrentContext().read(); if (getCurrentContext().isMapKey()) { throw new TException(SEQUENCE_AS_KEY_ILLEGAL); } JsonNode curElem = getCurrentContext().getCurrentChild(); if (!curElem.isArray()) { throw new TException("Expected JSON Array!"); } pushContext(new SequenceContext(curElem)); return curElem.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 { Class<?> fieldClass = getCurrentFieldClassIfIs(TEnum.class); if (fieldClass != null) { // Enum fields may be set by string, even though they represent integers. getCurrentContext().read(); JsonNode elem = getCurrentContext().getCurrentChild(); if (elem.isInt()) { return TypedParser.INTEGER.readFromJsonElement(elem); } else if (elem.isTextual()) { // All TEnum are enums @SuppressWarnings({ "unchecked", "rawtypes" }) TEnum tEnum = (TEnum) Enum.valueOf((Class<Enum>) fieldClass, TypedParser.STRING.readFromJsonElement(elem)); return tEnum.getValue(); } else { throw new TTransportException("invalid value type for enum field: " + elem.getNodeType() + " (" + elem + ')'); } } else { 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 readNameOrValue(TypedParser.BINARY); } /** * 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. * * <p>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(); JsonNode elem = getCurrentContext().getCurrentChild(); if (getCurrentContext().isMapKey()) { // Will throw a ClassCastException if this is not a JsonPrimitive string return ch.readFromString(elem.asText()); } else { return ch.readFromJsonElement(elem); } } /** * Read in the root node if it has not yet been read. */ private void readRoot() throws IOException { if (root != null) { return; } ByteArrayOutputStream content = new ByteArrayOutputStream(); byte[] buffer = new byte[READ_BUFFER_SIZE]; try { while (trans_.read(buffer, 0, READ_BUFFER_SIZE) > 0) { content.write(buffer); } } catch (TTransportException e) { if (TTransportException.END_OF_FILE != e.getType()) { throw new IOException(e); } } root = OBJECT_MAPPER.readTree(content.toByteArray()); } /** * 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(); } /** * Return the current parsing context. */ private JsonGenerator getCurrentWriter() { return writers.peek().writer; } private String getWriterString() throws TException { WriterByteArrayOutputStream wbaos = writers.peek(); String ret; try { wbaos.writer.flush(); ret = new String(wbaos.baos.toByteArray()); wbaos.writer.close(); } catch (IOException e) { throw new TException(e); } return ret; } private Class<?> getCurrentFieldClassIfIs(Class<?> classToMatch) { if (currentFieldClass.isEmpty() || currentFieldClass.peek() == null) { return null; } Class<?> classToCheck = currentFieldClass.peek(); if (classToMatch.isAssignableFrom(classToCheck)) { return classToCheck; } return null; } private void pushWriter(ByteArrayOutputStream baos) { JsonGenerator generator; try { generator = OBJECT_MAPPER.getFactory().createGenerator(baos, JsonEncoding.UTF8) .useDefaultPrettyPrinter(); } catch (IOException e) { // Can't happen, using a byte stream. throw new IllegalStateException(e); } WriterByteArrayOutputStream wbaos = new WriterByteArrayOutputStream(generator, baos); writers.push(wbaos); } private void popWriter() { writers.pop(); } private static final class WriterByteArrayOutputStream { final JsonGenerator writer; final ByteArrayOutputStream baos; private WriterByteArrayOutputStream(JsonGenerator writer, ByteArrayOutputStream baos) { this.writer = writer; this.baos = baos; } } /** * Factory. */ public static class Factory implements TProtocolFactory { private static final long serialVersionUID = -5607714914895109618L; @Override public TProtocol getProtocol(TTransport trans) { return new TTextProtocol(trans); } } /** * 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 transport @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. reset(); } } }