Current section

Files

Jump to
zotonic_mod_export src support export_encoder.erl
Raw

src/support/export_encoder.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2016-2024 Marc Worrell
%% @doc Interface with the export encoders.
%% @end
%% Copyright 2016-2024 Marc Worrell
%%
%% Licensed 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.
-module(export_encoder).
-author("Marc Worrell <marc@worrell.nl>").
-export([
content_types/1,
content_types_dispatch/2,
stream/4,
lookup_header/2,
lookup_value/2,
lookup_value/3
]).
-record(stream_state, {
id,
is_query,
content_type,
dispatch,
encoder,
encoder_state,
exporter_state
}).
-include_lib("zotonic_core/include/zotonic.hrl").
content_types(_Context) ->
lists:usort(
lists:foldl(
fun(Encoder, Acc) ->
Acc ++ Encoder:mime()
end,
[],
encoders())).
%% todo: let the encoder check against the category of the id
content_types_dispatch(_Id, _Context) ->
lists:flatten(
lists:map(
fun(Encoder) ->
Mimes = Encoder:mime(),
Type = hd(Encoder:extension()),
[ {Mime, {export_rsc, [{type, Type}]}} || Mime <- Mimes ]
end,
encoders())).
stream(Id, ContentType, Options, Context) ->
Enc = select_encoder(ContentType, encoders()),
EncOpts = [
{id, Id},
{dispatch, proplists:get_value(dispatch, Options)},
{is_query, proplists:get_value(is_query, Options, false)},
{is_raw, proplists:get_value(is_raw, Options, false)}
],
{ok, EncState} = Enc:init(EncOpts, Context),
StreamState = #stream_state{
id = Id,
is_query = proplists:get_value(is_query, EncOpts),
content_type = ContentType,
dispatch = proplists:get_value(dispatch, EncOpts),
encoder = Enc,
encoder_state = EncState
},
{stream, {<<>>, fun() -> do_header(StreamState, Context) end}}.
select_encoder(_CT, []) ->
export_encoder_csv;
select_encoder(CT, [M|Ms]) ->
case lists:member(CT, M:mime()) of
true -> M;
false -> select_encoder(CT,Ms)
end.
encoders() ->
[
export_encoder_json,
export_encoder_csv,
export_encoder_xlsx,
export_encoder_ics,
export_encoder_atom,
export_encoder_bert,
export_encoder_ubf
].
do_header(StreamState, Context) ->
case export_helper:call(#export_resource_header{
id=StreamState#stream_state.id,
content_type=StreamState#stream_state.content_type,
dispatch=StreamState#stream_state.dispatch
},
Context)
of
undefined ->
do_header_1(undefined, StreamState, Context);
{ok, Header, State} ->
do_header_1(Header, StreamState#stream_state{exporter_state=State}, Context);
{ok, Header} ->
do_header_1(Header, StreamState#stream_state{exporter_state=undefined}, Context)
end.
do_header_1(Header, #stream_state{encoder=E, encoder_state=ES} = StreamState, Context) ->
{ok, Data, ES1} = E:header(Header, ES, Context),
StreamState1 = StreamState#stream_state{encoder_state=ES1},
case is_empty(Data) of
true -> do_body(StreamState1, Context);
false -> {Data, fun() -> do_body(StreamState1, Context) end}
end.
is_empty(undefined) -> true;
is_empty([]) -> true;
is_empty(<<>>) -> true;
is_empty(_) -> false.
do_body(#stream_state{is_query=true, id=Id} = StreamState, Context) ->
#search_result{result=Ids} = z_search:search(
<<"query">>, #{ <<"query_id">> => Id },
1, 50000,
#{ is_count_rows => false, is_single_page => true },
Context),
do_body_data(Ids, StreamState, Context);
do_body(StreamState, Context) ->
case export_helper:call(#export_resource_data{
id=StreamState#stream_state.id,
content_type=StreamState#stream_state.content_type,
dispatch=StreamState#stream_state.dispatch,
state=StreamState#stream_state.exporter_state
},
Context)
of
undefined when StreamState#stream_state.id =/= undefined ->
do_body_data([StreamState#stream_state.id], StreamState, Context);
undefined ->
do_body_data([], StreamState, Context);
{ok, List} when is_list(List) ->
do_body_data(List, StreamState, Context);
{ok, List, NewState} when is_list(List) ->
do_body_data(List, StreamState#stream_state{exporter_state=NewState}, Context)
end.
do_body_data([], StreamState, Context) ->
do_footer(StreamState, Context);
do_body_data(List, StreamState, Context) ->
stream_body_rows(List, StreamState, Context).
stream_body_rows([], #stream_state{ exporter_state = undefined } = StreamState, Context) ->
do_footer(StreamState, Context);
stream_body_rows([], StreamState, Context) ->
do_body(StreamState, Context);
stream_body_rows([RowItem|NextRows], StreamState, Context) ->
{IoData, NewStreamState} = body_encode_row(RowItem, StreamState, Context),
DataBin = iolist_to_binary(IoData),
NextFun = case NextRows of
[] when NewStreamState#stream_state.exporter_state =:= undefined ->
fun() -> do_footer(NewStreamState, Context) end;
[] ->
fun() -> do_body(NewStreamState, Context) end;
_ ->
fun() -> stream_body_rows(NextRows, NewStreamState, Context) end
end,
case is_empty(DataBin) of
true -> NextFun();
false -> {DataBin, NextFun}
end.
%% TODO: replace 'encode' with 'row' -> it is about a row of values for a resource
body_encode_row(Item, StreamState, Context) ->
case export_helper:call(#export_resource_encode{
id=StreamState#stream_state.id,
content_type=StreamState#stream_state.content_type,
dispatch=StreamState#stream_state.dispatch,
state=StreamState#stream_state.exporter_state,
data=Item
},
Context)
of
undefined ->
encode_data_row(Item, StreamState, Context);
{ok, Row} ->
encode_data_row(Row, StreamState, Context);
{ok, Row, ExporterState} ->
encode_data_row(Row, StreamState#stream_state{exporter_state=ExporterState}, Context)
end.
encode_data_row(RowOrId, #stream_state{encoder=E, encoder_state=ES} = StreamState, Context) ->
{ok, Data, ES1} = E:row(RowOrId, ES, Context),
{Data, StreamState#stream_state{encoder_state=ES1}}.
do_footer(#stream_state{encoder=E, encoder_state=ES} = StreamState, Context) ->
case export_helper:call(#export_resource_footer{
id=StreamState#stream_state.id,
dispatch=StreamState#stream_state.dispatch,
content_type=StreamState#stream_state.content_type,
state=StreamState#stream_state.exporter_state},
Context)
of
undefined ->
{ok, Data} = E:footer(undefined, ES, Context),
{Data, done};
{ok, Row} ->
{ok, Data} = E:footer(Row, ES, Context),
{Data, done}
end.
%% @doc Used to lookup the header name, a header might be a tuple with lookup key and a textual representation.
lookup_header(#trans{} = Tr, Context) ->
z_trans:lookup_fallback(Tr, Context);
lookup_header(Key, _Context) ->
export_value:header(Key).
%% @doc Used to lookup the row value, a key might be a tuple with lookup key and a textual representation.
lookup_value(#trans{} = Tr, Context) ->
z_trans:lookup_fallback(Tr, Context);
lookup_value([Id, Value], Context) ->
lookup_value(Id, Value, Context);
lookup_value(Value, _Context) ->
Value.
lookup_value(_Id, #trans{} = Tr, Context) ->
z_trans:lookup_fallback(Tr, Context);
lookup_value(Id, Value, Context) ->
export_value:value(Id, Value, Context).