diff options
Diffstat (limited to 'src/xmpp_json.erl')
-rw-r--r-- | src/xmpp_json.erl | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/src/xmpp_json.erl b/src/xmpp_json.erl new file mode 100644 index 000000000..b10b5d323 --- /dev/null +++ b/src/xmpp_json.erl @@ -0,0 +1,417 @@ +%%%---------------------------------------------------------------------- +%%% 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-2013 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]). + +-include("jlib.hrl"). + +%%% FROM JSON TO XML + +from_json({[{<<"stream">>, _Attr}] = Elems}) -> + parse_start(Elems); +from_json({Elems}) -> + {xmlstreamelement, hd(from_json2({Elems}))}. + +from_json2({Elems}) -> + lists:map(fun parse_json/1, Elems). + +parse_start([{BinName, {JAttrs}}]) -> + Name = (BinName), + {FullName, Attrs} = lists:foldl(fun ({<<"xml">>, + {XML}}, + {N, Attrs}) -> + XmlAttrs = + parse_json_special_attrs(<<"xml">>, + XML), + {N, lists:merge(Attrs, XmlAttrs)}; + ({<<"xmlns">>, {XMLNS}}, + {N, Attrs}) -> + XmlNsAttrs = + parse_json_special_attrs(<<"xmlns">>, + XMLNS), + {N, lists:merge(Attrs, XmlNsAttrs)}; + ({<<"$$">>, BaseNS}, {N, Attrs}) -> + {<<((BaseNS))/binary, ":", + N/binary>>, + 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) -> + #xmlel{name = (Name), attrs = [], + children = [{xmlcdata, CData}]}; +parse_json({Name, CDatas}) when is_list(CDatas) -> + lists:map(fun (CData) -> + #xmlel{name = (Name), attrs = [], + children = [{xmlcdata, CData}]} + end, + CDatas); +parse_json({BinName, {JAttrs}}) -> + Name = (BinName), + {FullName, Attrs, SubEls} = lists:foldl(fun ({<<"$">>, + Cdata}, + {N, Attrs, _SubEls}) + when is_binary(Cdata) -> + {N, Attrs, + [{xmlcdata, Cdata}]}; + ({<<"$">>, {Elems}}, + {N, Attrs, _SubEls}) -> + SE = + lists:map(fun parse_json/1, + Elems), + {N, Attrs, + lists:flatten(SE)}; + ({<<"xml">>, {XML}}, + {N, Attrs, SubEls}) -> + XmlAttrs = + parse_json_special_attrs(<<"xml">>, + XML), + {N, + lists:merge(Attrs, + XmlAttrs), + SubEls}; + ({<<"xmlns">>, {XMLNS}}, + {N, Attrs, SubEls}) -> + XmlNsAttrs = + parse_json_special_attrs(<<"xmlns">>, + XMLNS), + {N, + lists:merge(Attrs, + XmlNsAttrs), + SubEls}; + ({Key, {[]}}, + {N, Attrs, SubEls}) -> + {N, Attrs, + [#xmlel{name = ib2tol(Key), + attrs = [], + children = []} + | SubEls]}; + ({Key, Value}, + {N, Attrs, SubEls}) -> + {N, + [{(Key), ib2tol(Value)} + | Attrs], + SubEls} + end, + {Name, [], []}, JAttrs), + #xmlel{name = FullName, attrs = Attrs, + children = SubEls}. + +parse_json_special_attrs(Prefix, XMLNS) -> + lists:reverse(lists:map(fun ({<<"$">>, Value}) -> + {Prefix, ib2tol(Value)}; + ({<<"@", NS/binary>>, Value}) -> + {<<Prefix/binary, ":", ((NS))/binary>>, + ib2tol(Value)} + end, + XMLNS)). + +to_json({xmlstreamelement, XMLElement}) -> + to_json(XMLElement); +to_json(#xmlel{attrs = [], children = []}) -> + {[]}; +to_json(#xmlel{name = Name, attrs = [], + children = [{xmlcdata, Cdata}]}) -> + {SName, JsonAttrs2} = parse_namespace(Name, []), + {[{SName, Cdata} | JsonAttrs2]}; +to_json({xmlstreamstart, Name, Attrs}) -> + JsonAttrs = parse_attrs(Attrs), + {SName, Members2} = parse_namespace(Name, JsonAttrs), + {[{SName, {Members2}}]}; +to_json(#xmlel{name = Name, attrs = Attrs, + children = SubEls}) -> + JsonAttrs = parse_attrs(Attrs), + Members = case parse_subels(SubEls) of + [] -> JsonAttrs; + Elems -> [{<<"$">>, Elems} | JsonAttrs] + end, + {SName, Members2} = parse_namespace(Name, Members), + {[{SName, {Members2}}]}. + +parse_namespace(Name, AttrsList) -> + {Name, AttrsList}. + +parse_subels([{xmlcdata, Cdata}]) -> Cdata; +parse_subels([]) -> []; +parse_subels(SubEls) -> + {lists:reverse(lists:foldl(fun (#xmlel{name = SName, + attrs = [], + children = [{xmlcdata, UCdata}]}, + Acc) -> + Cdata = UCdata, + Name = 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; + (#xmlel{name = SName} = Elem, Acc) -> + E = case to_json(Elem) of + {[{_, ToKeep}]} -> ToKeep; + {[]} = Empty -> Empty + end, + [{SName, E} | Acc]; + ({xmlcdata, <<"\n">>}, Acc) -> Acc + end, + [], SubEls))}. + +parse_attrs(XmlAttrs) -> + {Normal, XMLNS} = lists:foldl(fun ({<<"xmlns">>, NS}, + {Attrs, XMLNS}) -> + {Attrs, [{<<"$">>, NS} | XMLNS]}; + ({<<"xmlns:", Short/binary>>, NS}, + {Attrs, XMLNS}) -> + AttrName = <<$@, Short/binary>>, + {Attrs, + [{AttrName, NS} + | XMLNS]}; + ({<<"xml:", Short/binary>>, Val}, + {Attrs, XMLNS}) -> + AttrName = <<$@, Short/binary>>, + {[{<<"xml">>, + {[{AttrName, Val}]}} + | Attrs], + XMLNS}; + ({K, V}, {Attrs, XMLNS}) -> + {[{K, V} | Attrs], XMLNS} + end, + {[], []}, XmlAttrs), + case XMLNS of + [{<<"$">>, NS}] -> [{<<"xmlns">>, NS} | Normal]; + [] -> Normal; + _ -> [{<<"xmlns">>, {XMLNS}} | Normal] + end. + +ib2tol(Bin) when is_binary(Bin) -> Bin; +ib2tol(Integer) when is_integer(Integer) -> + jlib:integer_to_binary(Integer). + +%% +%% 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). + +to_text_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, attrs = [], + children = [{xmlcdata, <<"txt-value">>}]}}, + Out = {[{<<"tag">>, <<"txt-value">>}]}, + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +to_tag_with_recursive_tags_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, attrs = [], + children = + [#xmlel{name = <<"tag2">>, attrs = [], + children = [{xmlcdata, <<"txt-value">>}]}, + #xmlel{name = <<"tag3">>, attrs = [], + children = + [#xmlel{name = <<"tag4">>, attrs = [], + children = + [{xmlcdata, + <<"txt2-value">>}]}]}]}}, + Out = {[{<<"tag">>, + {[{<<"$">>, + {[{<<"tag2">>, <<"txt-value">>}, + {<<"tag3">>, + {[{<<"$">>, + {[{<<"tag4">>, <<"txt2-value">>}]}}]}}]}}]}}]}, + io:format("~n~p", [from_json(Out)]), + io:format("~n~p", [to_json(In)]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +multiple_text_value_tags_as_array_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, attrs = [], + children = + [#xmlel{name = <<"tag2">>, attrs = [], + children = [{xmlcdata, <<"txt-value">>}]}, + #xmlel{name = <<"tag2">>, attrs = [], + children = [{xmlcdata, <<"txt-value2">>}]}]}}, + Out = {[{<<"tag">>, + {[{<<"$">>, + {[{<<"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))). + +tag_attr_no_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, + attrs = [{<<"attr">>, <<"attr-value">>}], + children = []}}, + Out = {[{<<"tag">>, + {[{<<"attr">>, <<"attr-value">>}]}}]}, + io:format("~s", [jiffy: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 + +tag_with_namespace_no_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, + attrs = [{<<"xmlns:ns">>, <<"ns-value">>}], + children = []}}, + Out = {[{<<"tag">>, + {[{<<"xmlns">>, + {[{<<"@ns">>, <<"ns-value">>}]}}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +two_namespaces_tag_no_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, + attrs = + [{<<"xmlns:ns">>, <<"ns-value">>}, + {<<"xmlns">>, <<"root-value">>}], + children = []}}, + Out = {[{<<"tag">>, + {[{<<"xmlns">>, + {[{<<"$">>, <<"root-value">>}, + {<<"@ns">>, <<"ns-value">>}]}}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +namespaced_tag_no_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"ns:tag">>, + attrs = [{<<"attr">>, <<"attr-value">>}], + children = []}}, + Out = {[{<<"ns:tag">>, + {[{<<"attr">>, <<"attr-value">>}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +tag_with_attribute_and_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, + attrs = [{<<"attr">>, <<"attr-value">>}], + children = [{xmlcdata, <<"txt-value">>}]}}, + Out = {[{<<"tag">>, + {[{<<"$">>, <<"txt-value">>}, + {<<"attr">>, <<"attr-value">>}]}}]}, + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +namespaced_tag_with_value_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"ns:tag">>, + attrs = [{<<"attr">>, <<"attr-value">>}], + children = [{xmlcdata, <<"txt-value">>}]}}, + Out = {[{<<"ns:tag">>, + {[{<<"$">>, <<"txt-value">>}, + {<<"attr">>, <<"attr-value">>}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +xml_lang_attr_test() -> + In = {xmlstreamelement, + #xmlel{name = <<"tag">>, + attrs = [{<<"xml:lang">>, <<"en">>}], children = []}}, + Out = {[{<<"tag">>, + {[{<<"xml">>, {[{<<"@lang">>, <<"en">>}]}}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +xmlns_tag_with_value_test() -> + Out = {[{<<"response">>, + {[{<<"$">>, <<"dXNlcm5hbWU9I">>}, + {<<"xmlns">>, + <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]}}]}, + Out2 = {[{<<"response">>, + {[{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}, + {<<"$">>, <<"dXNlcm5hbWU9I">>}]}}]}, + In = {xmlstreamelement, + #xmlel{name = <<"response">>, + attrs = + [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}], + children = [{xmlcdata, <<"dXNlcm5hbWU9I">>}]}}, + io:format("~s", [jiffy: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, + #xmlel{name = <<"failure">>, + attrs = + [{<<"xmlns">>, <<"urn:ietf:params:xml:ns:xmpp-sasl">>}], + children = + [#xmlel{name = <<"not-authorized">>, attrs = [], + children = []}]}}, + Out = {[{<<"failure">>, + {[{<<"$">>, + {[{<<"not-authorized">>, {[]}}]}}, + {<<"xmlns">>, + <<"urn:ietf:params:xml:ns:xmpp-sasl">>}]}}]}, + io:format("~s", [jiffy: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 = {[{<<"stream">>, + {[{<<"xml">>, {[{<<"@lang">>, <<"en">>}]}}]}}]}, + io:format("~s", [jiffy:encode(to_json(In))]), + ?assertEqual(Out, (to_json(In))), + ?assertEqual(In, (from_json(Out))). + +-endif. |