diff options
Diffstat (limited to 'src/web/xmpp_json.erl')
-rw-r--r-- | src/web/xmpp_json.erl | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/src/web/xmpp_json.erl b/src/web/xmpp_json.erl new file mode 100644 index 000000000..6fae2ab08 --- /dev/null +++ b/src/web/xmpp_json.erl @@ -0,0 +1,362 @@ +%%%---------------------------------------------------------------------- +%%% File : xmpp_json.erl +%%% Author : Eric Cestari <ecestari@process-one.net> +%%% Purpose : Converts {xmlelement,Name, A, Sub} to/from JSON as per protoxep +%%% Created : 09-20-2010 +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2010 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License +%%% along with this program; if not, write to the Free Software +%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA +%%% 02111-1307 USA + +-module (xmpp_json). + +-export([to_json/1, from_json/1]). + + + +%%% FROM JSON TO XML + +from_json({struct, [{<<"stream">>, _Attr}]=Elems}) -> + parse_start(Elems); + +from_json({struct, Elems}) -> + {xmlstreamelement, hd(from_json2({struct, Elems}))}. + +from_json2({struct, Elems}) -> + lists:map(fun parse_json/1, Elems). + +parse_start([{BinName, {struct, JAttrs}}]) -> + Name = binary_to_list(BinName), + {FullName, Attrs} = lists:foldl( + fun({<<"xml">>, {struct, XML}}, {N, Attrs}) -> + XmlAttrs = parse_json_special_attrs("xml", XML), + {N, lists:merge(Attrs, XmlAttrs)}; + ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs}) -> + XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS), + {N, lists:merge(Attrs, XmlNsAttrs)}; + ({<<"$$">>, BaseNS}, {N, Attrs})-> + {binary_to_list(BaseNS)++":"++N, Attrs}; + ({Key, Value}, {N, Attrs})-> + {N, [{ib2tol(Key), ib2tol(Value)}|Attrs]} + end, {Name, []}, JAttrs), + {xmlstreamstart, FullName, Attrs}. + +parse_json({Name, CData}) when is_binary(CData)-> + {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]}; + +parse_json({Name, CDatas}) when is_list(CDatas)-> + lists:map(fun(CData)-> + {xmlelement, binary_to_list(Name), [], [{xmlcdata, CData}]} + end, CDatas); + +parse_json({BinName, {struct, JAttrs}}) -> + Name = binary_to_list(BinName), + {FullName, Attrs, SubEls} = lists:foldl( + fun({<<"$">>, Cdata}, {N, Attrs, _SubEls}) when is_binary(Cdata)-> + {N, Attrs, [{xmlcdata, Cdata}]}; + ({<<"$">>, {struct, Elems}}, {N, Attrs, _SubEls}) -> + SE = lists:map(fun parse_json/1, Elems), + {N, Attrs, lists:flatten(SE)}; % due to 4.2.3.3 + ({<<"xml">>, {struct, XML}}, {N, Attrs, SubEls}) -> + XmlAttrs = parse_json_special_attrs("xml", XML), + {N, lists:merge(Attrs, XmlAttrs), SubEls}; + ({<<"xmlns">>, {struct, XMLNS}}, {N, Attrs, SubEls}) -> + XmlNsAttrs = parse_json_special_attrs("xmlns", XMLNS), + {N, lists:merge(Attrs, XmlNsAttrs), SubEls}; + ({Key, {struct, []}}, {N, Attrs, SubEls})-> + {N, Attrs, [{xmlelement, ib2tol(Key), [], []}|SubEls]}; + ({Key, Value}, {N, Attrs, SubEls})-> + {N, [{binary_to_list(Key), ib2tol(Value)}|Attrs], SubEls} + end, {Name, [], []}, JAttrs), + {xmlelement, FullName, Attrs, SubEls}. + +parse_json_special_attrs(Prefix, XMLNS)-> + lists:reverse(lists:map( + fun({<<"$">>, Value})-> + {Prefix, ib2tol(Value)}; + ({<<"@",NS/binary>>, Value})-> + {Prefix ++ ":"++binary_to_list(NS), ib2tol(Value)} + end, XMLNS)). + +%%% FROM XML TO JSON +to_json({xmlstreamelement, XMLElement})-> + to_json(XMLElement); +to_json({xmlelement, _Name, [], []})-> + {struct, []}; +to_json({xmlelement, Name, [], [{xmlcdata, Cdata}]})-> + {SName, JsonAttrs2} = parse_namespace(Name, []), + {struct, [{SName, Cdata}|JsonAttrs2]}; +to_json({xmlstreamstart, Name, Attrs})-> + JsonAttrs = parse_attrs(Attrs), + {SName, Members2} = parse_namespace(Name, JsonAttrs), + {struct, [{SName, {struct, Members2}}]}; +to_json({xmlelement, Name, Attrs, SubEls})-> + JsonAttrs = parse_attrs(Attrs), + Members = case parse_subels(SubEls) of + [] -> + JsonAttrs; + [Elem] -> + [{<<"$">>,Elem}|JsonAttrs]; + Elems -> + [{<<"$">>,Elems}|JsonAttrs] + end, + {SName, Members2} = parse_namespace(Name, Members), + {struct, [{SName, {struct, Members2}}]}. + +parse_namespace(Name, AttrsList)-> + {l2b(Name), AttrsList}. + +parse_subels([{xmlcdata, Cdata}])-> + l2b(Cdata); +parse_subels([])-> + []; +parse_subels(SubEls)-> + {struct, lists:reverse(lists:foldl( + fun({xmlelement, SName, [], [{xmlcdata, UCdata}]}, Acc)-> + Cdata = l2b(UCdata), + Name = l2b(SName), + case lists:keyfind(Name, 1, Acc) of + {Name, PrevCdata} when is_binary(PrevCdata) -> + Acc1 = lists:keydelete(Name, 1, Acc), + [{Name,[PrevCdata, Cdata]} | Acc1]; + {Name, CDatas} when is_list(CDatas) -> + Acc1 = lists:keydelete(Name, 1, Acc), + [{Name,lists:append(CDatas, [Cdata])} | Acc1]; + _ -> + [{Name, Cdata}| Acc] + end; + ({xmlelement, SName, _, _} = Elem, Acc) -> + E = case to_json(Elem) of %TODO There could be a better way to iterate + {struct, [{_, ToKeep}]} -> ToKeep; + {struct, []} = Empty -> Empty + end, + [{l2b(SName), E}|Acc]; + ({xmlcdata,<<"\n">>}, Acc) -> + Acc + end,[], SubEls))}. + + +parse_attrs(XmlAttrs)-> + {Normal, XMLNS} = lists:foldl( + fun({"xmlns", NS}, {Attrs, XMLNS}) -> + {Attrs,[{<<"$">>, l2b(NS)}| XMLNS]}; + ({"xmlns:" ++ Short, NS}, {Attrs, XMLNS})-> + AttrName = iolist_to_binary([<<"@">>,l2b(Short)]), + {Attrs,[{AttrName, list_to_binary(NS)}| XMLNS]}; + ({"xml:" ++ Short, Val}, {Attrs, XMLNS})-> + % TODO currently tolerates only one xml:* attr per element + AttrName = iolist_to_binary([<<"@">>,l2b(Short)]), + {[{<<"xml">>,{struct, [{AttrName, l2b(Val)}]}}|Attrs], XMLNS}; + ({K, V}, {Attrs, XMLNS})-> + {[{l2b(K), l2b(V)}|Attrs], XMLNS} + end,{[], []}, XmlAttrs), + + case XMLNS of + [{<<"$">>, NS}]-> + [{<<"xmlns">>, NS}|Normal]; + []-> + Normal; + _ -> + [{<<"xmlns">>,{struct, XMLNS} }| Normal] + end. + +l2b(List) when is_list(List) -> list_to_binary(List); +l2b(Bin) when is_binary(Bin) -> Bin. + +ib2tol(Bin) when is_binary(Bin) -> binary_to_list(Bin ); +ib2tol(Integer) when is_integer(Integer) -> integer_to_list(Integer); +ib2tol(List) when is_list(List) -> List. + +%% +%% Tests +%% erlc -DTEST web/xmpp_json.erl && erl -pa web/ -run xmpp_json test -run init stop -noshell +-include_lib("eunit/include/eunit.hrl"). +-ifdef(TEST). + +% 4.2.3.1 Tag with text value +to_text_value_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"tag">>, <<"txt-value">>}]}, + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.2 Tag with recursive tags +to_tag_with_recursive_tags_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], + [{xmlelement,"tag2",[], [{xmlcdata, <<"txt-value">>}]}, + {xmlelement,"tag3",[], [ + {xmlelement,"tag4",[], [{xmlcdata, <<"txt2-value">>}]}]}]}}, + Out = {struct, [{<<"tag">>, + {struct, [{<<"$">>, + {struct, [ + {<<"tag2">>,<<"txt-value">>}, + {<<"tag3">>,{struct, [{<<"$">>,{struct, [{<<"tag4">>,<<"txt2-value">>}]}}]}} + ]} + }]} + }] + }, + %io:format("~n~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~n~p", [from_json(Out)]), + io:format("~n~p", [to_json(In)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.3 Multiple text value tags as array +multiple_text_value_tags_as_array_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [], [ + {xmlelement,"tag2",[], [ + {xmlcdata, <<"txt-value">>}]}, + {xmlelement,"tag2",[], [ + {xmlcdata, <<"txt-value2">>}]}]}}, + Out = {struct, [{<<"tag">>, + {struct, [{<<"$">>, + {struct, [{<<"tag2">>, + [<<"txt-value">>, <<"txt-value2">>]}]} + }]} + }] + }, + io:format("~p~n", [to_json(In)]), + io:format("~p~n", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.4 Tag with attribute, no value +tag_attr_no_value_test() -> + In = {xmlstreamelement, {xmlelement, "tag", [{"attr", "attr-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~p", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.5 Tag with multiple attributes as array, no value +% Not wellformed XML. + +% 4.2.3.6 Tags as array with unique attributes, no value + + +% 4.2.3.7 Tag with namespace attribute, no value +tag_with_namespace_no_value_test()-> + In = {xmlstreamelement, {xmlelement, "tag", [{"xmlns:ns", "ns-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xmlns">>,{struct, [{<<"@ns">>, <<"ns-value">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + + +% 4.2.3.8 Tag with many attributes to namespace, no value +two_namespaces_tag_no_value_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"xmlns:ns", "ns-value"}, + {"xmlns", "root-value"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xmlns">>,{struct, [ + {<<"$">>, <<"root-value">>}, + {<<"@ns">>, <<"ns-value">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.9 Tag with namespace attribute, no value +% Removed namespace handling. More complex on both sides. +namespaced_tag_no_value_test()-> + In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], []}}, + Out = {struct, [{<<"ns:tag">>, {struct, [ + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.10 Tag with attribute and text value +tag_with_attribute_and_value_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"attr", "attr-value"}], + [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"$">>, <<"txt-value">>}, + {<<"attr">>,<<"attr-value">>} + ]}}]}, + %io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +% 4.2.3.11 Namespace tag with attribute and text value +% Removed namespace handling. More complex on both sides +namespaced_tag_with_value_test()-> + In = {xmlstreamelement,{xmlelement, "ns:tag", [{"attr", "attr-value"}], [{xmlcdata, <<"txt-value">>}]}}, + Out = {struct, [{<<"ns:tag">>, {struct, [ + {<<"$">>,<<"txt-value">>}, + {<<"attr">>,<<"attr-value">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xml_lang_attr_test()-> + In = {xmlstreamelement,{xmlelement, "tag", [{"xml:lang", "en"}], []}}, + Out = {struct, [{<<"tag">>, {struct, [ + {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xmlns_tag_with_value_test()-> + Out = {struct,[{<<"response">>, + {struct,[{<<"$">>,<<"dXNlcm5hbWU9I">>}, + {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]}} + ]}, + Out2 = {struct,[{<<"response">>, + {struct,[{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}, + {<<"$">>,<<"dXNlcm5hbWU9I">>} + ]}} + ]}, + In = {xmlstreamelement,{xmlelement,"response", + [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}], + [{xmlcdata, <<"dXNlcm5hbWU9I">>}]}}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)), + ?assertEqual(In, from_json(Out2)). + +no_attr_no_value_test()-> + In = {xmlstreamelement, {xmlelement,"failure", + [{"xmlns","urn:ietf:params:xml:ns:xmpp-sasl"}], + [{xmlelement,"not-authorized",[],[]}]}}, + Out = {struct, [{<<"failure">>,{struct, [ + {<<"$">>, {struct, [{<<"not-authorized">>, {struct, []}}]}}, + {<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + io:format("~p~n", [to_json(In)]), + io:format("~p~n", [from_json(Out)]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). + +xmlstream_test()-> + In = {xmlstreamstart, "stream", [{"xml:lang", "en"}]}, + Out = {struct, [{<<"stream">>, {struct, [ + {<<"xml">>,{struct,[{<<"@lang">>,<<"en">>}]}} + ]}}]}, + io:format("~p", [list_to_binary(mochijson2:encode(to_json(In)))]), + ?assertEqual(Out, to_json(In)), + ?assertEqual(In, from_json(Out)). +-endif.
\ No newline at end of file |