| % 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. |
| |
| % Reads CouchDB's ini file and gets queried for configuration parameters. |
| % This module is initialized with a list of ini files that it consecutively |
| % reads Key/Value pairs from and saves them in an ets table. If more an one |
| % ini file is specified, the last one is used to write changes that are made |
| % with store/2 back to that ini file. |
| |
| -module(config). |
| -behaviour(gen_server). |
| |
| -export([start_link/1, stop/0]). |
| -export([all/0, get/1, get/2, get/3, set/3, set/4, delete/2, delete/3]). |
| -export([listen_for_changes/2]). |
| -export([parse_ini_file/1]). |
| |
| -export([init/1, terminate/2, code_change/3]). |
| -export([handle_call/3, handle_cast/2, handle_info/2]). |
| |
| -record(config, { |
| notify_funs=[], |
| write_filename=undefined |
| }). |
| |
| |
| start_link(IniFiles) -> |
| gen_server:start_link({local, ?MODULE}, ?MODULE, IniFiles, []). |
| |
| stop() -> |
| gen_server:cast(?MODULE, stop). |
| |
| |
| all() -> |
| lists:sort(gen_server:call(?MODULE, all, infinity)). |
| |
| |
| get(Section) when is_binary(Section) -> |
| ?MODULE:get(binary_to_list(Section)); |
| get(Section) -> |
| Matches = ets:match(?MODULE, {{Section, '$1'}, '$2'}), |
| [{Key, Value} || [Key, Value] <- Matches]. |
| |
| get(Section, Key) -> |
| ?MODULE:get(Section, Key, undefined). |
| |
| get(Section, Key, Default) when is_binary(Section) and is_binary(Key) -> |
| ?MODULE:get(binary_to_list(Section), binary_to_list(Key), Default); |
| get(Section, Key, Default) -> |
| case ets:lookup(?MODULE, {Section, Key}) of |
| [] -> Default; |
| [{_, Match}] -> Match |
| end. |
| |
| set(Section, Key, Value) -> |
| ?MODULE:set(Section, Key, Value, true). |
| |
| set(Section, Key, Value, Persist) when is_binary(Section) and is_binary(Key) -> |
| ?MODULE:set(binary_to_list(Section), binary_to_list(Key), Value, Persist); |
| set(Section, Key, Value, Persist) -> |
| gen_server:call(?MODULE, {set, Section, Key, Value, Persist}). |
| |
| |
| delete(Section, Key) when is_binary(Section) and is_binary(Key) -> |
| delete(binary_to_list(Section), binary_to_list(Key)); |
| delete(Section, Key) -> |
| delete(Section, Key, true). |
| |
| delete(Section, Key, Persist) when is_binary(Section) and is_binary(Key) -> |
| delete(binary_to_list(Section), binary_to_list(Key), Persist); |
| delete(Section, Key, Persist) -> |
| gen_server:call(?MODULE, {delete, Section, Key, Persist}). |
| |
| listen_for_changes(CallbackModule, InitialState) -> |
| config_listener:start(CallbackModule, InitialState). |
| |
| init(IniFiles) -> |
| ets:new(?MODULE, [named_table, set, protected]), |
| lists:map(fun(IniFile) -> |
| {ok, ParsedIniValues} = parse_ini_file(IniFile), |
| ets:insert(?MODULE, ParsedIniValues) |
| end, IniFiles), |
| WriteFile = case IniFiles of |
| [_|_] -> lists:last(IniFiles); |
| _ -> undefined |
| end, |
| debug_config(), |
| {ok, #config{write_filename=WriteFile}}. |
| |
| |
| terminate(_Reason, _State) -> |
| ok. |
| |
| |
| handle_call(all, _From, Config) -> |
| Resp = lists:sort((ets:tab2list(?MODULE))), |
| {reply, Resp, Config}; |
| handle_call({set, Sec, Key, Val, Persist}, _From, Config) -> |
| true = ets:insert(?MODULE, {{Sec, Key}, Val}), |
| twig:log(notice, "~p: [~s] ~s set to ~s", [?MODULE, Sec, Key, Val]), |
| case {Persist, Config#config.write_filename} of |
| {true, undefined} -> |
| ok; |
| {true, FileName} -> |
| config_writer:save_to_file({{Sec, Key}, Val}, FileName); |
| _ -> |
| ok |
| end, |
| Event = {config_change, Sec, Key, Val, Persist}, |
| gen_event:sync_notify(config_event, Event), |
| {reply, ok, Config}; |
| handle_call({delete, Sec, Key, Persist}, _From, Config) -> |
| true = ets:delete(?MODULE, {Sec,Key}), |
| twig:log(notice, "~p: [~s] ~s deleted", [?MODULE, Sec, Key]), |
| case {Persist, Config#config.write_filename} of |
| {true, undefined} -> |
| ok; |
| {true, FileName} -> |
| config_writer:save_to_file({{Sec, Key}, ""}, FileName); |
| _ -> |
| ok |
| end, |
| Event = {config_change, Sec, Key, deleted, Persist}, |
| gen_event:sync_notify(config_event, Event), |
| {reply, ok, Config}. |
| |
| |
| handle_cast(stop, State) -> |
| {stop, normal, State}; |
| handle_cast(_Msg, State) -> |
| {noreply, State}. |
| |
| handle_info(Info, State) -> |
| twig:log(error, "config:handle_info Info: ~p~n", [Info]), |
| {noreply, State}. |
| |
| code_change(_OldVsn, State, _Extra) -> |
| {ok, State}. |
| |
| |
| parse_ini_file(IniFile) -> |
| IniFilename = config_util:abs_pathname(IniFile), |
| IniBin = |
| case file:read_file(IniFilename) of |
| {ok, IniBin0} -> |
| IniBin0; |
| {error, enoent} -> |
| Fmt = "Couldn't find server configuration file ~s.", |
| Msg = list_to_binary(io_lib:format(Fmt, [IniFilename])), |
| twig:log(error, "~s~n", [Msg]), |
| throw({startup_error, Msg}) |
| end, |
| |
| Lines = re:split(IniBin, "\r\n|\n|\r|\032", [{return, list}]), |
| {_, ParsedIniValues} = |
| lists:foldl(fun(Line, {AccSectionName, AccValues}) -> |
| case string:strip(Line) of |
| "[" ++ Rest -> |
| case re:split(Rest, "\\]", [{return, list}]) of |
| [NewSectionName, ""] -> |
| {NewSectionName, AccValues}; |
| _Else -> % end bracket not at end, ignore this line |
| {AccSectionName, AccValues} |
| end; |
| ";" ++ _Comment -> |
| {AccSectionName, AccValues}; |
| Line2 -> |
| case re:split(Line2, "\s?=\s?", [{return, list}]) of |
| [Value] -> |
| MultiLineValuePart = case re:run(Line, "^ \\S", []) of |
| {match, _} -> |
| true; |
| _ -> |
| false |
| end, |
| case {MultiLineValuePart, AccValues} of |
| {true, [{{_, ValueName}, PrevValue} | AccValuesRest]} -> |
| % remove comment |
| case re:split(Value, " ;|\t;", [{return, list}]) of |
| [[]] -> |
| % empty line |
| {AccSectionName, AccValues}; |
| [LineValue | _Rest] -> |
| E = {{AccSectionName, ValueName}, |
| PrevValue ++ " " ++ LineValue}, |
| {AccSectionName, [E | AccValuesRest]} |
| end; |
| _ -> |
| {AccSectionName, AccValues} |
| end; |
| [""|_LineValues] -> % line begins with "=", ignore |
| {AccSectionName, AccValues}; |
| [ValueName|LineValues] -> % yeehaw, got a line! |
| RemainingLine = config_util:implode(LineValues, "="), |
| % removes comments |
| case re:split(RemainingLine, " ;|\t;", [{return, list}]) of |
| [[]] -> |
| % empty line means delete this key |
| ets:delete(?MODULE, {AccSectionName, ValueName}), |
| {AccSectionName, AccValues}; |
| [LineValue | _Rest] -> |
| {AccSectionName, |
| [{{AccSectionName, ValueName}, LineValue} | AccValues]} |
| end |
| end |
| end |
| end, {"", []}, Lines), |
| {ok, ParsedIniValues}. |
| |
| |
| debug_config() -> |
| case ?MODULE:get("log", "level") of |
| "debug" -> |
| io:format("Configuration Settings:~n", []), |
| lists:foreach(fun({{Mod, Key}, Val}) -> |
| io:format(" [~s] ~s=~p~n", [Mod, Key, Val]) |
| end, lists:sort(ets:tab2list(?MODULE))); |
| _ -> |
| ok |
| end. |
| |