%%%---------------------------------------------------------------------- %%% File : xmpp_json.erl %%% Author : Eric Cestari %%% 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.