| %% |
| %% Licensed to the Apache Software Foundation (ASF) under one |
| %% or more contributor license agreements. See the NOTICE file |
| %% distributed with this work for additional information |
| %% regarding copyright ownership. The ASF licenses this file |
| %% to you under the Apache License, Version 2.0 (the |
| %% "License"); you may not use this file except in compliance |
| %% with the License. You may obtain a copy of the License 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. |
| %% |
| %% The JSON protocol implementation was created by |
| %% Peter Neumark <neumark.peter@gmail.com> based on |
| %% the binary protocol implementation. |
| |
| -module(thrift_json_protocol). |
| |
| -behaviour(thrift_protocol). |
| |
| -include("thrift_constants.hrl"). |
| -include("thrift_protocol.hrl"). |
| |
| -export([new/1, new/2, |
| read/2, |
| write/2, |
| flush_transport/1, |
| close_transport/1, |
| new_protocol_factory/2 |
| ]). |
| |
| -record(json_context, { |
| % the type of json_context: array or object |
| type, |
| % fields read or written |
| fields_processed = 0 |
| }). |
| |
| -record(json_protocol, { |
| transport, |
| context_stack = [], |
| jsx |
| }). |
| -type state() :: #json_protocol{}. |
| -include("thrift_protocol_behaviour.hrl"). |
| |
| -define(VERSION_1, 1). |
| -define(JSON_DOUBLE_PRECISION, 16). |
| |
| typeid_to_json(?tType_BOOL) -> "tf"; |
| typeid_to_json(?tType_BYTE) -> "i8"; |
| typeid_to_json(?tType_DOUBLE) -> "dbl"; |
| typeid_to_json(?tType_I16) -> "i16"; |
| typeid_to_json(?tType_I32) -> "i32"; |
| typeid_to_json(?tType_I64) -> "i64"; |
| typeid_to_json(?tType_STRING) -> "str"; |
| typeid_to_json(?tType_STRUCT) -> "rec"; |
| typeid_to_json(?tType_MAP) -> "map"; |
| typeid_to_json(?tType_SET) -> "set"; |
| typeid_to_json(?tType_LIST) -> "lst". |
| |
| json_to_typeid("tf") -> ?tType_BOOL; |
| json_to_typeid("i8") -> ?tType_BYTE; |
| json_to_typeid("dbl") -> ?tType_DOUBLE; |
| json_to_typeid("i16") -> ?tType_I16; |
| json_to_typeid("i32") -> ?tType_I32; |
| json_to_typeid("i64") -> ?tType_I64; |
| json_to_typeid("str") -> ?tType_STRING; |
| json_to_typeid("rec") -> ?tType_STRUCT; |
| json_to_typeid("map") -> ?tType_MAP; |
| json_to_typeid("set") -> ?tType_SET; |
| json_to_typeid("lst") -> ?tType_LIST. |
| |
| start_context(object) -> "{"; |
| start_context(array) -> "[". |
| |
| end_context(object) -> "}"; |
| end_context(array) -> "]". |
| |
| |
| new(Transport) -> |
| new(Transport, _Options = []). |
| |
| new(Transport, _Options) -> |
| State = #json_protocol{transport = Transport}, |
| thrift_protocol:new(?MODULE, State). |
| |
| flush_transport(This = #json_protocol{transport = Transport}) -> |
| {NewTransport, Result} = thrift_transport:flush(Transport), |
| {This#json_protocol{ |
| transport = NewTransport, |
| context_stack = [] |
| }, Result}. |
| |
| close_transport(This = #json_protocol{transport = Transport}) -> |
| {NewTransport, Result} = thrift_transport:close(Transport), |
| {This#json_protocol{ |
| transport = NewTransport, |
| context_stack = [], |
| jsx = undefined |
| }, Result}. |
| |
| %%% |
| %%% instance methods |
| %%% |
| % places a new context on the stack: |
| write(#json_protocol{context_stack = Stack} = State0, {enter_context, Type}) -> |
| {State1, ok} = write_values(State0, [{context_pre_item, false}]), |
| State2 = State1#json_protocol{context_stack = [ |
| #json_context{type=Type}|Stack]}, |
| write_values(State2, [list_to_binary(start_context(Type))]); |
| |
| % removes the topmost context from stack |
| write(#json_protocol{context_stack = [CurrCtxt|Stack]} = State0, {exit_context}) -> |
| Type = CurrCtxt#json_context.type, |
| State1 = State0#json_protocol{context_stack = Stack}, |
| write_values(State1, [ |
| list_to_binary(end_context(Type)), |
| {context_post_item, false} |
| ]); |
| |
| % writes necessary prelude to field or container depending on current context |
| write(#json_protocol{context_stack = []} = This0, |
| {context_pre_item, _}) -> {This0, ok}; |
| write(#json_protocol{context_stack = [Context|_CtxtTail]} = This0, |
| {context_pre_item, MayNeedQuotes}) -> |
| FieldNo = Context#json_context.fields_processed, |
| CtxtType = Context#json_context.type, |
| Rem = FieldNo rem 2, |
| case {CtxtType, FieldNo, Rem, MayNeedQuotes} of |
| {array, N, _, _} when N > 0 -> % array element (not first) |
| write(This0, <<",">>); |
| {object, 0, _, true} -> % non-string object key (first) |
| write(This0, <<"\"">>); |
| {object, N, 0, true} when N > 0 -> % non-string object key (not first) |
| write(This0, <<",\"">>); |
| {object, N, 0, false} when N > 0-> % string object key (not first) |
| write(This0, <<",">>); |
| _ -> % no pre-field necessary |
| {This0, ok} |
| end; |
| |
| % writes necessary postlude to field or container depending on current context |
| write(#json_protocol{context_stack = []} = This0, |
| {context_post_item, _}) -> {This0, ok}; |
| write(#json_protocol{context_stack = [Context|CtxtTail]} = This0, |
| {context_post_item, MayNeedQuotes}) -> |
| FieldNo = Context#json_context.fields_processed, |
| CtxtType = Context#json_context.type, |
| Rem = FieldNo rem 2, |
| {This1, ok} = case {CtxtType, Rem, MayNeedQuotes} of |
| {object, 0, true} -> % non-string object key |
| write(This0, <<"\":">>); |
| {object, 0, false} -> % string object key |
| write(This0, <<":">>); |
| _ -> % no pre-field necessary |
| {This0, ok} |
| end, |
| NewContext = Context#json_context{fields_processed = FieldNo + 1}, |
| {This1#json_protocol{context_stack=[NewContext|CtxtTail]}, ok}; |
| |
| write(This0, #protocol_message_begin{ |
| name = Name, |
| type = Type, |
| seqid = Seqid}) -> |
| write_values(This0, [ |
| {enter_context, array}, |
| {i32, ?VERSION_1}, |
| {string, Name}, |
| {i32, Type}, |
| {i32, Seqid} |
| ]); |
| |
| write(This, message_end) -> |
| write_values(This, [{exit_context}]); |
| |
| % Example field expression: "1":{"dbl":3.14} |
| write(This0, #protocol_field_begin{ |
| name = _Name, |
| type = Type, |
| id = Id}) -> |
| write_values(This0, [ |
| % entering 'outer' object |
| {i16, Id}, |
| % entering 'outer' object |
| {enter_context, object}, |
| {string, typeid_to_json(Type)} |
| ]); |
| |
| write(This, field_stop) -> |
| {This, ok}; |
| |
| write(This, field_end) -> |
| write_values(This,[{exit_context}]); |
| |
| % Example message with map: [1,"testMap",1,0,{"1":{"map":["i32","i32",3,{"7":77,"8":88,"9":99}]}}] |
| write(This0, #protocol_map_begin{ |
| ktype = Ktype, |
| vtype = Vtype, |
| size = Size}) -> |
| write_values(This0, [ |
| {enter_context, array}, |
| {string, typeid_to_json(Ktype)}, |
| {string, typeid_to_json(Vtype)}, |
| {i32, Size}, |
| {enter_context, object} |
| ]); |
| |
| write(This, map_end) -> |
| write_values(This,[ |
| {exit_context}, |
| {exit_context} |
| ]); |
| |
| write(This0, #protocol_list_begin{ |
| etype = Etype, |
| size = Size}) -> |
| write_values(This0, [ |
| {enter_context, array}, |
| {string, typeid_to_json(Etype)}, |
| {i32, Size} |
| ]); |
| |
| write(This, list_end) -> |
| write_values(This,[ |
| {exit_context} |
| ]); |
| |
| % example message with set: [1,"testSet",1,0,{"1":{"set":["i32",3,1,2,3]}}] |
| write(This0, #protocol_set_begin{ |
| etype = Etype, |
| size = Size}) -> |
| write_values(This0, [ |
| {enter_context, array}, |
| {string, typeid_to_json(Etype)}, |
| {i32, Size} |
| ]); |
| |
| write(This, set_end) -> |
| write_values(This,[ |
| {exit_context} |
| ]); |
| % example message with struct: [1,"testStruct",1,0,{"1":{"rec":{"1":{"str":"worked"},"4":{"i8":1},"9":{"i32":1073741824},"11":{"i64":1152921504606847000}}}}] |
| write(This, #protocol_struct_begin{}) -> |
| write_values(This, [ |
| {enter_context, object} |
| ]); |
| |
| write(This, struct_end) -> |
| write_values(This,[ |
| {exit_context} |
| ]); |
| |
| write(This, {bool, true}) -> write_values(This, [ |
| {context_pre_item, true}, |
| <<"true">>, |
| {context_post_item, true} |
| ]); |
| |
| write(This, {bool, false}) -> write_values(This, [ |
| {context_pre_item, true}, |
| <<"false">>, |
| {context_post_item, true} |
| ]); |
| |
| write(This, {byte, Byte}) -> write_values(This, [ |
| {context_pre_item, true}, |
| list_to_binary(integer_to_list(Byte)), |
| {context_post_item, true} |
| ]); |
| |
| write(This, {i16, I16}) -> |
| write(This, {byte, I16}); |
| |
| write(This, {i32, I32}) -> |
| write(This, {byte, I32}); |
| |
| write(This, {i64, I64}) -> |
| write(This, {byte, I64}); |
| |
| write(This, {double, Double}) -> write_values(This, [ |
| {context_pre_item, true}, |
| list_to_binary(io_lib:format("~.*f", [?JSON_DOUBLE_PRECISION,Double])), |
| {context_post_item, true} |
| ]); |
| |
| write(This0, {string, Str}) -> write_values(This0, [ |
| {context_pre_item, false}, |
| case is_binary(Str) of |
| true -> Str; |
| false -> <<"\"", (list_to_binary(Str))/binary, "\"">> |
| end, |
| {context_post_item, false} |
| ]); |
| |
| %% TODO: binary fields should be base64 encoded? |
| |
| %% Data :: iolist() |
| write(This = #json_protocol{transport = Trans}, Data) -> |
| %io:format("Data ~p Ctxt ~p~n~n", [Data, This#json_protocol.context_stack]), |
| {NewTransport, Result} = thrift_transport:write(Trans, Data), |
| {This#json_protocol{transport = NewTransport}, Result}. |
| |
| write_values(This0, ValueList) -> |
| FinalState = lists:foldl( |
| fun(Val, ThisIn) -> |
| {ThisOut, ok} = write(ThisIn, Val), |
| ThisOut |
| end, |
| This0, |
| ValueList), |
| {FinalState, ok}. |
| |
| %% I wish the erlang version of the transport interface included a |
| %% read_all function (like eg. the java implementation). Since it doesn't, |
| %% here's my version (even though it probably shouldn't be in this file). |
| %% |
| %% The resulting binary is immediately send to the JSX stream parser. |
| %% Subsequent calls to read actually operate on the events returned by JSX. |
| read_all(#json_protocol{transport = Transport0} = State) -> |
| {Transport1, Bin} = read_all_1(Transport0, []), |
| P = thrift_json_parser:parser(), |
| [First|Rest] = P(Bin), |
| State#json_protocol{ |
| transport = Transport1, |
| jsx = {event, First, Rest} |
| }. |
| |
| read_all_1(Transport0, IoList) -> |
| {Transport1, Result} = thrift_transport:read(Transport0, 1), |
| case Result of |
| {ok, <<>>} -> % nothing read: assume we're done |
| {Transport1, iolist_to_binary(lists:reverse(IoList))}; |
| {ok, Data} -> % character successfully read; read more |
| read_all_1(Transport1, [Data|IoList]); |
| {error, 'EOF'} -> % we're done |
| {Transport1, iolist_to_binary(lists:reverse(IoList))} |
| end. |
| |
| % Expect reads an event from the JSX event stream. It receives an event or data |
| % type as input. Comparing the read event from the one is was passed, it |
| % returns an error if something other than the expected value is encountered. |
| % Expect also maintains the context stack in #json_protocol. |
| expect(#json_protocol{jsx={event, {Type, Data}=Ev, [Next|Rest]}}=State, ExpectedType) -> |
| NextState = State#json_protocol{jsx={event, Next, Rest}}, |
| case Type == ExpectedType of |
| true -> |
| {NextState, {ok, convert_data(Type, Data)}}; |
| false -> |
| {NextState, {error, {unexpected_json_event, Ev}}} |
| end; |
| |
| expect(#json_protocol{jsx={event, Event, Next}}=State, ExpectedEvent) -> |
| expect(State#json_protocol{jsx={event, {Event, none}, Next}}, ExpectedEvent). |
| |
| convert_data(integer, I) -> list_to_integer(I); |
| convert_data(float, F) -> list_to_float(F); |
| convert_data(_, D) -> D. |
| |
| expect_many(State, ExpectedList) -> |
| expect_many_1(State, ExpectedList, [], ok). |
| |
| expect_many_1(State, [], ResultList, Status) -> |
| {State, {Status, lists:reverse(ResultList)}}; |
| expect_many_1(State, [Expected|ExpTail], ResultList, _PrevStatus) -> |
| {State1, {Status, Data}} = expect(State, Expected), |
| NewResultList = [Data|ResultList], |
| case Status of |
| % in case of error, end prematurely |
| error -> expect_many_1(State1, [], NewResultList, Status); |
| ok -> expect_many_1(State1, ExpTail, NewResultList, Status) |
| end. |
| |
| % wrapper around expect to make life easier for container opening/closing functions |
| expect_nodata(This, ExpectedList) -> |
| case expect_many(This, ExpectedList) of |
| {State, {ok, _}} -> |
| {State, ok}; |
| Error -> |
| Error |
| end. |
| |
| read_field(#json_protocol{jsx={event, Field, [Next|Rest]}} = State) -> |
| NewState = State#json_protocol{jsx={event, Next, Rest}}, |
| {NewState, Field}. |
| |
| read(This0, message_begin) -> |
| % call read_all to get the contents of the transport buffer into JSX. |
| This1 = read_all(This0), |
| case expect_many(This1, |
| [start_array, integer, string, integer, integer]) of |
| {This2, {ok, [_, Version, Name, Type, SeqId]}} -> |
| case Version =:= ?VERSION_1 of |
| true -> |
| {This2, #protocol_message_begin{name = Name, |
| type = Type, |
| seqid = SeqId}}; |
| false -> |
| {This2, {error, no_json_protocol_version}} |
| end; |
| Other -> Other |
| end; |
| |
| read(This, message_end) -> |
| expect_nodata(This, [end_array]); |
| |
| read(This, struct_begin) -> |
| expect_nodata(This, [start_object]); |
| |
| read(This, struct_end) -> |
| expect_nodata(This, [end_object]); |
| |
| read(This0, field_begin) -> |
| {This1, Read} = expect_many(This0, |
| [%field id |
| key, |
| % {} surrounding field |
| start_object, |
| % type of field |
| key]), |
| case Read of |
| {ok, [FieldIdStr, _, FieldType]} -> |
| {This1, #protocol_field_begin{ |
| type = json_to_typeid(FieldType), |
| id = list_to_integer(FieldIdStr)}}; % TODO: do we need to wrap this in a try/catch? |
| {error,[{unexpected_json_event, {end_object,none}}]} -> |
| {This1, #protocol_field_begin{type = ?tType_STOP}}; |
| Other -> |
| io:format("**** OTHER branch selected ****"), |
| {This1, Other} |
| end; |
| |
| read(This, field_end) -> |
| expect_nodata(This, [end_object]); |
| |
| % Example message with map: [1,"testMap",1,0,{"1":{"map":["i32","i32",3,{"7":77,"8":88,"9":99}]}}] |
| read(This0, map_begin) -> |
| case expect_many(This0, |
| [start_array, |
| % key type |
| string, |
| % value type |
| string, |
| % size |
| integer, |
| % the following object contains the map |
| start_object]) of |
| {This1, {ok, [_, Ktype, Vtype, Size, _]}} -> |
| {This1, #protocol_map_begin{ktype = Ktype, |
| vtype = Vtype, |
| size = Size}}; |
| Other -> Other |
| end; |
| |
| read(This, map_end) -> |
| expect_nodata(This, [end_object, end_array]); |
| |
| read(This0, list_begin) -> |
| case expect_many(This0, |
| [start_array, |
| % element type |
| string, |
| % size |
| integer]) of |
| {This1, {ok, [_, Etype, Size]}} -> |
| {This1, #protocol_list_begin{ |
| etype = Etype, |
| size = Size}}; |
| Other -> Other |
| end; |
| |
| read(This, list_end) -> |
| expect_nodata(This, [end_array]); |
| |
| % example message with set: [1,"testSet",1,0,{"1":{"set":["i32",3,1,2,3]}}] |
| read(This0, set_begin) -> |
| case expect_many(This0, |
| [start_array, |
| % element type |
| string, |
| % size |
| integer]) of |
| {This1, {ok, [_, Etype, Size]}} -> |
| {This1, #protocol_set_begin{ |
| etype = Etype, |
| size = Size}}; |
| Other -> Other |
| end; |
| |
| read(This, set_end) -> |
| expect_nodata(This, [end_array]); |
| |
| read(This0, field_stop) -> |
| {This0, ok}; |
| %% |
| |
| read(This0, bool) -> |
| {This1, Field} = read_field(This0), |
| Value = case Field of |
| {literal, I} -> |
| {ok, I}; |
| _Other -> |
| {error, unexpected_event_for_boolean} |
| end, |
| {This1, Value}; |
| |
| read(This0, byte) -> |
| {This1, Field} = read_field(This0), |
| Value = case Field of |
| {key, K} -> |
| {ok, list_to_integer(K)}; |
| {integer, I} -> |
| {ok, list_to_integer(I)}; |
| _Other -> |
| {error, unexpected_event_for_integer} |
| end, |
| {This1, Value}; |
| |
| read(This0, i16) -> |
| read(This0, byte); |
| |
| read(This0, i32) -> |
| read(This0, byte); |
| |
| read(This0, i64) -> |
| read(This0, byte); |
| |
| read(This0, double) -> |
| {This1, Field} = read_field(This0), |
| Value = case Field of |
| {float, I} -> |
| {ok, list_to_float(I)}; |
| _Other -> |
| {error, unexpected_event_for_double} |
| end, |
| {This1, Value}; |
| |
| % returns a binary directly, call binary_to_list if necessary |
| read(This0, string) -> |
| {This1, Field} = read_field(This0), |
| Value = case Field of |
| {string, I} -> |
| {ok, I}; |
| {key, J} -> |
| {ok, J}; |
| _Other -> |
| {error, unexpected_event_for_string} |
| end, |
| {This1, Value}. |
| |
| %%%% FACTORY GENERATION %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% |
| |
| %% returns a (fun() -> thrift_protocol()) |
| new_protocol_factory(TransportFactory, _Options) -> |
| % Only strice read/write are implemented |
| F = fun() -> |
| {ok, Transport} = TransportFactory(), |
| thrift_json_protocol:new(Transport, []) |
| end, |
| {ok, F}. |