%% -*- coding: utf-8 -*-
%% Copyright (c) 2022, Madalin Grigore-Enescu <https://github.com/ergenius> <https://ergenius.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(t__repository).
-author("Madalin Grigore-Enescu").
-include_lib("kernel/include/file.hrl").
-include("../include/t__.hrl").
-export([
update/2, update_force/2,
ets_table/2
]).
%% Prefix for repositories ETS tables
-define(T__ETS_PREFIX, "t__").
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% update
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec update(Application, Repository) -> {ok, NewRepository} | {error, Error} when
Application :: atom(),
Repository :: t__:repository(),
NewRepository :: t__:repository(),
Error :: term().
%% @doc Update the specified repository (only if file changes are detected).
update(Application, Repository = #t__repository{name = RepositoryName}) ->
case update_check(Repository) of
{true, NewFileList} ->
?T__LOG(debug, "Repository changed. Updating ETS tables.", [{application, Application}, {repository, RepositoryName}]),
update_force(Application, Repository, NewFileList);
false -> {ok, Repository};
Error ->
%% Most probable this is because repository directory:
%% - is invalid
%% - was deleted/no longer exists
%% Please notice that even if the repository directory is invalid
%% we support adding invalid repositories in the development mode.
RepositoryEtsTable = ets_table(Application, RepositoryName),
ets_table_create_or_cleanup(RepositoryEtsTable),
Error
end.
%%-------------------------------------------------------------
%% update_check
%%-------------------------------------------------------------
-spec update_check(Repository) -> false | {true, NewFilelist} | {error, Error} when
Repository :: t__:repository(),
NewFilelist :: {string(), file:file_info()},
Error :: term().
%% @doc Check repository files for updates
update_check(#t__repository{directory = Directory, files = Files}) ->
case t__utils:dir_list_files(Directory, [regular], [".po"]) of
{ok, NewFiles} ->
case erlang:is_list(Files) of
true -> update_check_files(Files, NewFiles);
false -> {true, NewFiles}
end;
Error2 -> Error2
end.
update_check_files(L1, L2) when length(L1) =/= length(L2) ->
{true, lists:keysort(1, L2)};
update_check_files(L1, L2) ->
%% L1 is already sorted
SL2 = lists:keysort(1, L2),
case update_check_compare(L1, SL2) of
true -> {true, SL2};
false -> false
end.
update_check_compare(
[{Filename1, #file_info{size = Size1, type = Type1, mtime = Mtime1}} | T1],
[{Filename2, #file_info{size = Size2, type = Type2, mtime = Mtime2}} | T2]) ->
case {Size1, Filename1, Type1, Mtime1} of
{Size2, Filename2, Type2, Mtime2} -> update_check_compare(T1, T2);
_ -> true
end;
update_check_compare([], []) -> false.
%%-------------------------------------------------------------
%% update_force
%%-------------------------------------------------------------
-spec update_force(Application, Repository) -> {ok, NewRepository} | {error, Error} when
Application :: atom(),
Repository :: t__:repository(),
NewRepository :: t__:repository(),
Error :: term().
%% @doc Update the application repository
update_force(Application, Repository) -> update_force(Application, Repository, undefined).
-spec update_force(Application, Repository, NewFiles) -> {ok, NewRepository} | {error, Error} when
Application :: atom(),
Repository :: t__:repository(),
NewFiles :: undefined | list(),
NewRepository :: t__:repository(),
Error :: term().
%% @doc Force update repository from the specified list of files.
%% If the file list is undefined, we get the file list from repository directory.
update_force(Application, Repository = #t__repository{
directory = Directory
}, undefined) ->
case t__utils:dir_list_files(Directory, [regular], [".po"]) of
{ok, NewFiles} -> update_force(Application, Repository, NewFiles);
Error -> Error
end;
update_force(Application, Repository = #t__repository{
name = RepositoryName
}, NewFiles) when erlang:is_list(NewFiles) ->
RepositoryEtsTable = ets_table(Application, RepositoryName),
ets_table_create_or_cleanup(RepositoryEtsTable),
update_po(NewFiles, RepositoryEtsTable),
{ok, Repository#t__repository{files = NewFiles}}.
%%-------------------------------------------------------------
%% update_po
%%-------------------------------------------------------------
%% @doc Iterate list of PO files and load them into the proper ETS tables
update_po([{Filename, _} | T], RepositoryEts) ->
try
case t__po:file_read(Filename) of
{ok, Header, Msgs} ->
FilenameLanguage = filename:basename(Filename, ".po"),
Language = proplists:get_value("Language", Header, FilenameLanguage),
try
true = ets:insert(RepositoryEts, {{po_header, Language}, Header}),
true = update_po_messages(Msgs, Language, RepositoryEts)
catch Exception:Reason ->
?T__LOG(error, "Fail inserting messages into ETS table.",
[{exception, {Exception, Reason}}, {filename, Filename}, {ets, RepositoryEts}])
end;
Error ->
?T__LOG(error, "Error parsing PO file.",
[{filename, Filename}, {error, Error}])
end
catch Exception1:Reason1 ->
?T__LOG(error, "Exception parsing file", [{exception, {Exception1, Reason1}}, {filename, Filename}])
end,
update_po(T, RepositoryEts);
update_po([], _RepositoryEts) -> ok.
%% @doc Insert messages for the language into the specified ETS table
update_po_messages([{{Context, Msgs}, {Comments, Msgstr}} | T], Language, RepositoryEts) ->
true = ets:insert(RepositoryEts, {{po, Language, Context, Msgs}, {Comments, Msgstr}}),
true = ets:insert(RepositoryEts, {{pot, Context, Msgs}, Comments}),
update_po_messages(T, Language, RepositoryEts);
update_po_messages([], _Language, _RepositoryEts) -> true.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% ets_table
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec ets_table(Application, RepositoryName) -> TableName when
Application :: string() | binary() | atom(),
RepositoryName :: string() | binary() | atom(),
TableName :: string().
%% @doc Return the ETS table holding information for the specified application repository
ets_table(Application, RepositoryName) ->
ApplicationL = t__utils:name_to_string(Application),
RepositoryName = t__utils:name_to_string(RepositoryName),
erlang:list_to_atom(?T__ETS_PREFIX ++ ApplicationL ++ "_" ++ RepositoryName).
%% @doc Create the repository ETS table if does not exist or cleanup the table otherwise.
%% If table exists this function delete all the PO headers and translated strings from the table.
ets_table_create_or_cleanup(Table) ->
case ets:info(Table) of
undefined ->
ets:new(Table, [set, named_table, public]),
ok;
_ ->
ets:foldl(
fun({Key, _Value}, _Acc) ->
case Key of
{pot, _Context, _Msg} -> ok;
_ ->
ets:delete(Table, Key),
ok
end
end, ok, Table)
end.