1% Licensed under the Apache License, Version 2.0 (the "License"); you may not
2% use this file except in compliance with the License. You may obtain a copy of
3% the License at
4%
5%   http://www.apache.org/licenses/LICENSE-2.0
6%
7% Unless required by applicable law or agreed to in writing, software
8% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10% License for the specific language governing permissions and limitations under
11% the License.
12
13% Reads CouchDB's ini file and gets queried for configuration parameters.
14% This module is initialized with a list of ini files that it consecutively
15% reads Key/Value pairs from and saves them in an ets table. If more an one
16% ini file is specified, the last one is used to write changes that are made
17% with store/2 back to that ini file.
18
19-module(couch_config).
20-behaviour(gen_server).
21
22-include("couch_db.hrl").
23
24
25-export([start_link/1, stop/0]).
26-export([all/0, get/1, get/2, get/3, set/3, set/4, delete/2, delete/3]).
27-export([register/1, register/2]).
28-export([parse_ini_file/1]).
29
30-export([init/1, terminate/2, code_change/3]).
31-export([handle_call/3, handle_cast/2, handle_info/2]).
32
33-record(config, {
34    notify_funs=[],
35    write_filename=undefined
36}).
37
38
39start_link(IniFiles) ->
40    gen_server:start_link({local, ?MODULE}, ?MODULE, IniFiles, []).
41
42stop() ->
43    gen_server:cast(?MODULE, stop).
44
45
46all() ->
47    lists:sort(gen_server:call(?MODULE, all, infinity)).
48
49
50get(Section) when is_binary(Section) ->
51    ?MODULE:get(?b2l(Section));
52get(Section) ->
53    Matches = ets:match(?MODULE, {{Section, '$1'}, '$2'}),
54    [{Key, Value} || [Key, Value] <- Matches].
55
56get(Section, Key) ->
57    ?MODULE:get(Section, Key, undefined).
58
59get(Section, Key, Default) when is_binary(Section) and is_binary(Key) ->
60    ?MODULE:get(?b2l(Section), ?b2l(Key), Default);
61get(Section, Key, Default) ->
62    case ets:lookup(?MODULE, {Section, Key}) of
63        [] -> Default;
64        [{_, Match}] -> Match
65    end.
66
67set(Section, Key, Value) ->
68    ?MODULE:set(Section, Key, Value, true).
69
70set(Section, Key, Value, Persist) when is_binary(Section) and is_binary(Key)  ->
71    ?MODULE:set(?b2l(Section), ?b2l(Key), Value, Persist);
72set(Section, Key, Value, Persist) ->
73    gen_server:call(?MODULE, {set, Section, Key, Value, Persist}).
74
75
76delete(Section, Key) when is_binary(Section) and is_binary(Key) ->
77    delete(?b2l(Section), ?b2l(Key));
78delete(Section, Key) ->
79    delete(Section, Key, true).
80
81delete(Section, Key, Persist) when is_binary(Section) and is_binary(Key) ->
82    delete(?b2l(Section), ?b2l(Key), Persist);
83delete(Section, Key, Persist) ->
84    gen_server:call(?MODULE, {delete, Section, Key, Persist}).
85
86
87register(Fun) ->
88    ?MODULE:register(Fun, self()).
89
90register(Fun, Pid) ->
91    gen_server:call(?MODULE, {register, Fun, Pid}).
92
93
94init(IniFiles) ->
95    ets:new(?MODULE, [named_table, set, protected]),
96    try
97        lists:map(fun(IniFile) ->
98            {ok, ParsedIniValues} = parse_ini_file(IniFile),
99            ets:insert(?MODULE, ParsedIniValues)
100        end, IniFiles),
101        WriteFile = case IniFiles of
102            [_|_] -> lists:last(IniFiles);
103            _ -> undefined
104        end,
105        {ok, #config{write_filename = WriteFile}}
106    catch _Tag:Error ->
107        {stop, Error}
108    end.
109
110
111terminate(_Reason, _State) ->
112    ok.
113
114
115handle_call(all, _From, Config) ->
116    Resp = lists:sort((ets:tab2list(?MODULE))),
117    {reply, Resp, Config};
118handle_call({set, Sec, Key, Val, Persist}, From, Config) ->
119    Result = case {Persist, Config#config.write_filename} of
120        {true, undefined} ->
121            ok;
122        {true, FileName} ->
123            couch_config_writer:save_to_file({{Sec, Key}, Val}, FileName);
124        _ ->
125            ok
126    end,
127    case Result of
128    ok ->
129        true = ets:insert(?MODULE, {{Sec, Key}, Val}),
130        spawn_link(fun() ->
131            [catch F(Sec, Key, Val, Persist) || {_Pid, F} <- Config#config.notify_funs],
132                gen_server:reply(From, ok)
133        end),
134        {noreply, Config};
135    _Error ->
136        {reply, Result, Config}
137    end;
138handle_call({delete, Sec, Key, Persist}, From, Config) ->
139    true = ets:delete(?MODULE, {Sec,Key}),
140    case {Persist, Config#config.write_filename} of
141        {true, undefined} ->
142            ok;
143        {true, FileName} ->
144            couch_config_writer:save_to_file({{Sec, Key}, ""}, FileName);
145        _ ->
146            ok
147    end,
148    spawn_link(fun() ->
149        [catch F(Sec, Key, deleted, Persist) || {_Pid, F} <- Config#config.notify_funs],
150            gen_server:reply(From, ok)
151    end),
152    {noreply, Config};
153handle_call({register, Fun, Pid}, _From, #config{notify_funs=PidFuns}=Config) ->
154    erlang:monitor(process, Pid),
155    % convert 1 and 2 arity to 3 arity
156    Fun2 =
157    case Fun of
158        _ when is_function(Fun, 1) ->
159            fun(Section, _Key, _Value, _Persist) -> Fun(Section) end;
160        _ when is_function(Fun, 2) ->
161            fun(Section, Key, _Value, _Persist) -> Fun(Section, Key) end;
162        _ when is_function(Fun, 3) ->
163            fun(Section, Key, Value, _Persist) -> Fun(Section, Key, Value) end;
164        _ when is_function(Fun, 4) ->
165            Fun
166    end,
167    {reply, ok, Config#config{notify_funs=[{Pid, Fun2} | PidFuns]}}.
168
169
170handle_cast(stop, State) ->
171    {stop, normal, State};
172handle_cast(_Msg, State) ->
173    {noreply, State}.
174
175handle_info({'DOWN', _, _, DownPid, _}, #config{notify_funs=PidFuns}=Config) ->
176    % remove any funs registered by the downed process
177    FilteredPidFuns = [{Pid,Fun} || {Pid,Fun} <- PidFuns, Pid /= DownPid],
178    {noreply, Config#config{notify_funs=FilteredPidFuns}}.
179
180code_change(_OldVsn, State, _Extra) ->
181    {ok, State}.
182
183
184parse_ini_file(IniFile) ->
185    IniFilename = couch_util:abs_pathname(IniFile),
186    IniBin =
187    case file2:read_file(IniFilename) of
188        {ok, IniBin0} ->
189            IniBin0;
190        {error, eacces} ->
191            throw({file_permission_error, IniFile});
192        {error, enoent} ->
193            Fmt = "Couldn't find server configuration file ~s.",
194            Msg = ?l2b(io_lib:format(Fmt, [IniFilename])),
195            ?LOG_ERROR("~s~n", [Msg]),
196            throw({startup_error, Msg})
197    end,
198
199    Lines = re:split(IniBin, "\r\n|\n|\r|\032", [{return, list}]),
200    {_, ParsedIniValues} =
201    lists:foldl(fun(Line, {AccSectionName, AccValues}) ->
202            case string:strip(Line) of
203            "[" ++ Rest ->
204                case re:split(Rest, "\\]", [{return, list}]) of
205                [NewSectionName, ""] ->
206                    {NewSectionName, AccValues};
207                _Else -> % end bracket not at end, ignore this line
208                    {AccSectionName, AccValues}
209                end;
210            ";" ++ _Comment ->
211                {AccSectionName, AccValues};
212            Line2 ->
213                case re:split(Line2, "\s?=\s?", [{return, list}]) of
214                [Value] ->
215                    MultiLineValuePart = case re:run(Line, "^ \\S", []) of
216                    {match, _} ->
217                        true;
218                    _ ->
219                        false
220                    end,
221                    case {MultiLineValuePart, AccValues} of
222                    {true, [{{_, ValueName}, PrevValue} | AccValuesRest]} ->
223                        % remove comment
224                        case re:split(Value, " ;|\t;", [{return, list}]) of
225                        [[]] ->
226                            % empty line
227                            {AccSectionName, AccValues};
228                        [LineValue | _Rest] ->
229                            E = {{AccSectionName, ValueName},
230                                PrevValue ++ " " ++ LineValue},
231                            {AccSectionName, [E | AccValuesRest]}
232                        end;
233                    _ ->
234                        {AccSectionName, AccValues}
235                    end;
236                [""|_LineValues] -> % line begins with "=", ignore
237                    {AccSectionName, AccValues};
238                [ValueName|LineValues] -> % yeehaw, got a line!
239                    RemainingLine = couch_util:implode(LineValues, "="),
240                    % removes comments
241                    case re:split(RemainingLine, " ;|\t;", [{return, list}]) of
242                    [[]] ->
243                        % empty line means delete this key
244                        ets:delete(?MODULE, {AccSectionName, ValueName}),
245                        {AccSectionName, AccValues};
246                    [LineValue | _Rest] ->
247                        {AccSectionName,
248                            [{{AccSectionName, ValueName}, LineValue} | AccValues]}
249                    end
250                end
251            end
252        end, {"", []}, Lines),
253    {ok, ParsedIniValues}.
254
255