%%%------------------------------------------------------------------- %%% @author Fred Youhanaie %%% @copyright 2024, Fred Youhanaie %%% @doc %%% %%% A set of functions to process `XML' files. See the overview doc %%% for further details. %%% %%% To start the process of parsing the XML file, the callback module %%% should call the `read/3' function. While `read/3' is scanning the %%% XML file it will call the appropriate handler functions. %%% %%% The callback module should provide three callback functions: %%% %%% %%% %%% All three callback handlers are passed the behaviour state, and should %%% return the state, optionally updated. %%% %%% @end %%% Created : 2024-10-12 by Fred Youhanaie %%%------------------------------------------------------------------- -module(gen_xml). -export([read/3, attr_map/1]). -include_lib("xmerl/include/xmerl.hrl"). -include_lib("kernel/include/logger.hrl"). %% record type used for the SAX `user_state' -record(state, {cb_module, cb_state}). %%-------------------------------------------------------------------- -type read_ret() :: {fatal_error, term(), term(), term(), term()} | {ok, State :: term()} | {error, Reason :: term()}. -export_type([read_ret/0]). %%-------------------------------------------------------------------- -callback handle_begin(Tag :: atom(), Attr :: term(), State :: term()) -> Result :: term(). -callback handle_end(Tag :: atom(), State :: term()) -> Result :: term(). -callback handle_text(Text :: string(), State :: term()) -> Result :: term(). %%-------------------------------------------------------------------- %% @doc Read in a valid XML file and process its elements using the %% supplied callback module. %% %% We expect the file to be a valid XML document, no validation is %% performed here. However, the supplied callback module can perform %% its own validation during the scan. %% %% We return success/failure result. In case of success, the handler's %% final state is returned. The failure reason may originate from the %% `xmerl_sax_parser' module. %% %% @end %%-------------------------------------------------------------------- -spec read(string(), atom(), term()) -> read_ret(). read(Filename, CB_module, CB_state) -> ?LOG_INFO("read: scan started File=~p.", [Filename]), Modes = [raw, read_ahead, read, binary, compressed], Result = case file:open(Filename, Modes) of {error, Reason} -> {error,{Filename, file:format_error(Reason)}}; {ok, IO_dev} -> Res = scan(IO_dev, CB_module, CB_state), file:close(IO_dev), Res end, %% collect the statistics and return the results ?LOG_INFO("read: Result=~p.", [Result]), Result. %%-------------------------------------------------------------------- %% @doc Scan and process an XML io stream. %% %% @end %%-------------------------------------------------------------------- -spec scan(file:io_device(), atom(), term()) -> read_ret(). scan(IO_dev, CB_module, CB_state) -> Scan_opts = [ {event_fun, fun event_cb/3}, {event_state, #state{cb_module=CB_module, cb_state=CB_state}}, {continuation_fun, fun continuation_cb/1}, {continuation_state, IO_dev} ], case xmerl_sax_parser:stream(<<>>, Scan_opts) of {ok, State, _Rest} -> {ok, State#state.cb_state}; Other_result -> Other_result end. %%-------------------------------------------------------------------- %% @doc The event handler for the SAX parser. %% %% We basically handle all the tag starts, ends and character %% contents, and ignore the rest. %% %% With all callback handlers, `handle_begin', `handle_end' and %% `handle_text', we supply the current state as the last argument to %% the handler, and use the returned result as the new state. %% %% For the `startElement' event we call `handle_begin/3' with the %% element's `Tag' as an atom, and `Attributes', as provided by %% `xmerl_sax_parser' event data. The handler can use the function %% `attr_map/1` to extract a map of `#{Key => Value}' pairs from %% `Attributes'. %% %% For the `endElement' event we call `handle_end/2' with the `Tag' %% and the current state. %% %% For the `characters' event we call `handle_text/2' with the element %% text and the current state. %% %% @end %%-------------------------------------------------------------------- -spec event_cb({atom(), string(), string(), string(), list()}, term(), term()) -> term(). event_cb({startElement, _Uri, LocalName, _QualName, Attr}, _Loc, #state{cb_module=CB_module, cb_state=CB_state0}=State) -> ?LOG_INFO("event_cb: startElement."), Tag = list_to_atom(LocalName), CB_state1 = CB_module:handle_begin(Tag, Attr, CB_state0), State#state{cb_state=CB_state1}; event_cb({endElement, _Uri, LocalName, _QualName}, _Loc, #state{cb_module=CB_module, cb_state=CB_state0}=State) -> ?LOG_INFO("event_cb: endElement."), Tag = list_to_atom(LocalName), CB_state1 = CB_module:handle_end(Tag, CB_state0), State#state{cb_state=CB_state1}; event_cb({characters, Text}, _Loc, #state{cb_module=CB_module, cb_state=CB_state0}=State) -> ?LOG_INFO("event_cb: characters."), CB_state1 = CB_module:handle_text(Text, CB_state0), State#state{cb_state=CB_state1}; event_cb(Event, _Loc, State) -> %% we ignore all the other events ?LOG_DEBUG("event_cb: IGNORED Ev=~p, St=~p.", [Event, State]), State. %%-------------------------------------------------------------------- %% @doc Return a map corresponding to the list of attributes from %% xmerl. %% %% We extract the attribute names and values, and ignore the rest. %% %% The attribute names are returned as atoms and the values as %% strings. %% %% @end %%-------------------------------------------------------------------- -spec attr_map(list()) -> map(). attr_map(Attr) -> Attr_to_pair = fun ({_, _, Name, Val}) -> {list_to_atom(Name), Val} end, Attr_list = lists:map(Attr_to_pair, Attr), maps:from_list(Attr_list). %%-------------------------------------------------------------------- %% @doc Continuation function required by `xmerl_sax_parser:stream/2'. %% %% @end %%-------------------------------------------------------------------- -spec continuation_cb(file:io_device()) -> {binary(), file:io_device()}. continuation_cb(IO_dev) -> case file:read(IO_dev, 1024) of eof -> {<<>>, IO_dev}; {ok, FileBin} -> {FileBin, IO_dev} end. %%--------------------------------------------------------------------