Current section
Files
Jump to
Current section
Files
src/tivan_page.erl
%%%-------------------------------------------------------------------
%%% @author danny
%%% @copyright (C) 2019, danny
%%% @doc
%%%
%%% @end
%%% Created : 2019-04-19 18:38:31.676004
%%%-------------------------------------------------------------------
-module(tivan_page).
-behaviour(gen_server).
-define(TIMEOUT, 600).
-define(OPTIONS, [public, compressed]).
%% API
-export([start_link/0
,new/0
,new/2
,put/2
,get/1
,get/2
,sort/2
,remove/1
,list/0
,purge/0
,info/1]).
%% gen_server callbacks
-export([init/1
,handle_call/3
,handle_cast/2
,handle_info/2
,terminate/2
,code_change/3]).
%%%===================================================================
%%% API
%%%===================================================================
%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @spec start_link() -> {ok, Pid} | ignore | {error, Error}
%% @end
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
new() -> new(page, #{}).
new(Name, Options) ->
CacheOptions = maps:get(cache_options, Options, ?OPTIONS),
case maps:get(manage, Options, false) of
true ->
do_new(Name, CacheOptions);
false ->
InactiveTimeout = maps:get(inactive_timeout, Options, ?TIMEOUT),
gen_server:call(?MODULE, {new, Name, CacheOptions, InactiveTimeout})
end.
put(Id, Objects) ->
case inspect(Id) of
{_, public} -> do_put(Id, Objects);
{caller, _} -> do_put(Id, Objects);
{?MODULE, _} -> gen_server:call(?MODULE, {put, Id, Objects});
Error -> Error
end.
get(Id) -> get(Id, #{}).
get(Id, Options) ->
case inspect(Id) of
{?MODULE, private} -> gen_server:call(?MODULE, {get, Id, Options});
{?MODULE, _} ->
gen_server:cast(?MODULE, {access, Id}),
do_get(Id, Options);
{caller, _} -> do_get(Id, Options);
Error -> Error
end.
sort(Id, SortArgs) ->
case inspect(Id) of
{?MODULE, public} ->
gen_server:cast(?MODULE, {access, Id}),
do_sort(Id, SortArgs);
{_, public} -> do_sort(Id, SortArgs);
{caller, _} -> do_sort(Id, SortArgs);
{?MODULE, _} -> gen_server:call(?MODULE, {sort, Id, SortArgs});
Error -> Error
end.
remove(Id) ->
case inspect(Id) of
{caller, _} -> do_remove(Id);
{?MODULE, _} -> gen_server:cast(?MODULE, {remove, Id});
Error -> Error
end.
list() -> gen_server:call(?MODULE, list).
purge() -> gen_server:cast(?MODULE, purge).
info(Id) ->
case ets:info(Id) of
undefined -> undefined;
InfoList -> maps:from_list(InfoList)
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([]) ->
{ok, #{}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%% {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call({new, Name, CacheOptions, InactiveTimeout}, _From, State) ->
Id = do_new(Name, CacheOptions),
StateNew = State#{Id => {erlang:system_time(second), InactiveTimeout}},
{reply, Id, StateNew};
handle_call({put, Id, Objects}, _From, State) ->
{_, InactiveTimeout} = maps:get(Id, State),
Reply = do_put(Id, Objects),
StateNew = State#{Id => {erlang:system_time(second), InactiveTimeout}},
{reply, Reply, StateNew};
handle_call({get, Id, Options}, _From, State) ->
{_, InactiveTimeout} = maps:get(Id, State),
Reply = do_get(Id, Options),
StateNew = State#{Id => {erlang:system_time(second), InactiveTimeout}},
{reply, Reply, StateNew};
handle_call({sort, Id, SortArgs}, _From, State) ->
{_, InactiveTimeout} = maps:get(Id, State),
Reply = do_sort(Id, SortArgs),
StateNew = State#{Id => {erlang:system_time(second), InactiveTimeout}},
{reply, Reply, StateNew};
handle_call({remove, Id}, _From, State) ->
Reply = do_remove(Id),
StateNew = maps:remove(Id, State),
{reply, Reply, StateNew};
handle_call(list, _From, State) ->
{reply, maps:keys(State), State};
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({access, Id}, State) ->
{_, Timeout} = maps:get(Id, State),
StateNew = State#{Id => {erlang:system_time(second), Timeout}},
{noreply, StateNew};
handle_cast({remove, Id}, State) ->
do_remove(Id),
StateNew = maps:remove(Id, State),
{noreply, StateNew};
handle_cast(purge, State) ->
StateNew = do_purge(State),
{noreply, StateNew};
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
do_new(Name, CacheOptions) ->
ets:new(Name, CacheOptions).
inspect(Id) ->
case ets:info(Id) of
undefined ->
{error, undefined};
InfoList ->
Access = proplists:get_value(protection, InfoList),
Self = self(),
Page = whereis(?MODULE),
case proplists:get_value(owner, InfoList) of
Self ->
{caller, Access};
Page ->
{?MODULE, Access};
_ ->
{error, not_allowed}
end
end.
do_put(Id, Objects) ->
lists:foldl(
fun(O, K) ->
ets:insert(Id, {K, O}),
K + 1
end,
1,
Objects
),
ok.
do_get(Id, #{search := Pattern}) ->
PatternLower = pattern_lowercase(Pattern),
ets:foldr(
fun({_, O}, Os) ->
OBin = object_to_lowercase(O),
case binary:match(OBin, PatternLower) of
nomatch -> Os;
_matches -> [O|Os]
end
end,
[],
Id
);
do_get(Id, Options) ->
Start = maps:get(start, Options, 1),
Stop = Start + maps:get(limit, Options, ets:info(Id, size)) - 1,
lists:foldr(
fun(K, Os) ->
case ets:lookup(Id, K) of
[] ->
Os;
[{K, O}] ->
[O|Os]
end
end,
[],
lists:seq(Start, Stop)
).
object_to_lowercase(Object) ->
list_to_binary(
string:lowercase(
lists:flatten(
io_lib:format("~p", [Object])
)
)
).
pattern_lowercase(Pattern) when is_list(Pattern) ->
[ pattern_lowercase(X) || X <- Pattern ];
pattern_lowercase(Pattern) when is_binary(Pattern) ->
string:lowercase(Pattern).
do_sort(Id, SortFun) when is_function(SortFun, 2) ->
ObjectsList = [ V || {_K, V} <- ets:tab2list(Id) ],
ObjectsListSorted = lists:sort(SortFun, ObjectsList),
reload(Id, ObjectsListSorted);
do_sort(Id, SortArgs) ->
case SortArgs of
{Key, Order} when Order == desc; Order == asc ->
case is_sort_required(Id, SortArgs) of
false ->
ok;
true ->
ObjectsList = lists:map(
fun({_K, #{Key := Kv} = V}) ->
{Kv, V};
({_K, V}) ->
{undefined, V}
end,
ets:tab2list(Id)
),
Fun = case Order of
desc -> fun({K1, _}, {K2, _}) -> K1 > K2 end;
_ -> fun({K1, _}, {K2, _}) -> K1 < K2 end
end,
ObjectsListSorted = [ V || {_, V} <- lists:sort(Fun, ObjectsList)],
reload(Id, ObjectsListSorted)
end;
Other ->
do_sort(Id, {Other, asc})
end.
is_sort_required(Id, {Key, Order}) ->
Name = list_to_atom(atom_to_list(Key) ++ "_" ++ atom_to_list(Order)),
case info(Id) of
undefined -> false;
#{named_table := true} -> true;
#{name := Name} -> false;
_ ->
ets:rename(Id, Name),
true
end.
reload(Id, ObjectsListSorted) ->
ets:delete_all_objects(Id),
lists:foldl(
fun(O, K) ->
ets:insert(Id, {K, O}),
K + 1
end,
1,
ObjectsListSorted
),
ok.
do_remove(Id) ->
ets:delete(Id).
do_purge(State) ->
TsNow = erlang:system_time(second),
maps:fold(
fun(Id, {Ts, T}, S) ->
if
TsNow - Ts > T ->
ets:delete(Id),
S;
true ->
S#{Id => {Ts, T}}
end
end,
#{},
State
).