diff options
author | Jan Lehnardt <jan@apache.org> | 2008-08-20 14:18:05 +0000 |
---|---|---|
committer | Jan Lehnardt <jan@apache.org> | 2008-08-20 14:18:05 +0000 |
commit | 83af9a01292b5324de651ed90199f61be8d9eae7 (patch) | |
tree | cce4408d58132b37f620e033ccc0c2fac57d93ae /src/couchdb | |
parent | 4700dd67f03c020c3799a4bf161eadfacbf420cb (diff) |
add missing files
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@687339 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/couchdb')
-rw-r--r-- | src/couchdb/couch_config.erl | 195 | ||||
-rw-r--r-- | src/couchdb/couch_config_writer.erl | 147 | ||||
-rw-r--r-- | src/couchdb/couch_db_update_notifier_sup.erl | 50 |
3 files changed, 392 insertions, 0 deletions
diff --git a/src/couchdb/couch_config.erl b/src/couchdb/couch_config.erl new file mode 100644 index 00000000..07d47dff --- /dev/null +++ b/src/couchdb/couch_config.erl @@ -0,0 +1,195 @@ +% 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. + +%% @doc 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(couch_config). +-include("couch_db.hrl"). + +-behaviour(gen_server). +-export([start_link/1, init/1, + handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). +-export([store/2, register/1, register/2, + get/1, get/2, + lookup_match/1, lookup_match/2, + all/0, unset/1, load_ini_file/1]). + +-record(config, + {notify_funs=[], + writeback_filename="" + }). + +%% Public API %% + +%% @type etstable() = integer(). + +start_link(IniFiles) -> gen_server:start_link({local, ?MODULE}, ?MODULE, IniFiles, []). + +%% @spec store(Key::any(), Value::any()) -> {ok, Tab::etsatable()} +%% @doc Public API function that triggers storage of a Key/Value pair into the +%% local ets table and writes it to the storage ini file. +store(Key, Value) -> gen_server:call(?MODULE, {store, [{Key, Value}]}). + +%% @spec get(Key::any()) -> Value::any() | undefined +%% @doc Returns the value that is stored under key::any() or undefined::atom() if no +%% such Key exists. +get(Key) -> + ?MODULE:get(Key, undefined). + +%% @spec get(Key::any(), Default::any()) -> Value::any() | Default +%% @doc Returns the value that is stored under key::any() or Default::any() if +%% no such Key exists. +get(Key, Default) -> + fix_lookup_result(ets:lookup(?MODULE, Key), Default). + +%% @spec lookup_match(Key::any()) -> Value::any() | undefined:atom() +%% @doc Lets you look for a Key's Value specifying a pattern that gets passed +%% to ets::match(). Returns undefined::atom() if no Key is found. +lookup_match(Key) -> gen_server:call(?MODULE, {lookup_match, Key}). + +%% @spec lookup_match(Key::any(), Default::any()) -> Value::any() | Default +%% @doc Lets you look for a Key's Value specifying a pattern that gets passed +%% to ets::match(). Returns Default::any() if no Key is found +lookup_match(Key, Default) -> gen_server:call(?MODULE, {lookup_match, Key, Default}). + +all() -> gen_server:call(?MODULE, all). + +register(Fun) -> gen_server:call(?MODULE, {register, Fun, self()}). + + +register(Fun, Pid) -> gen_server:call(?MODULE, {register, Fun, Pid}). + +%% @spec unset(Key::any) -> ok +%% @doc Public API call to remove the configuration entry from the internal +%% ets table. This change is _not_ written to the storage ini file. +unset(Key) -> gen_server:call(?MODULE, {unset, Key}). + +%% Private API %% + +%% @spec init(List::list([])) -> {ok, Tab::etsatable()} +%% @doc Creates a new ets table of the type "set". +init(IniFiles) -> + ets:new(?MODULE, [named_table, set, protected]), + [ok = load_ini_file(IniFile) || IniFile <- IniFiles], + {ok, #config{writeback_filename=lists:last(IniFiles)}}. + +%% @doc see store/2 +handle_call({store, KVs}, _From, Config) -> + [ok = insert_and_commit(Config, KV) || KV <- KVs], + {reply, ok, Config}; + + +%% @doc See init_value/2 +handle_call({init_value, Key, Value}, _From, Config) -> + Reply = ets:insert(?MODULE, {Key, Value}), + {reply, Reply, Config}; + +%% @doc See unset/1 +handle_call({unset, Key}, _From, Config) -> + ets:delete(?MODULE, Key), + {reply, ok, Config}; + + +%% @doc See lookup_match/2 +handle_call({lookup_match, Key, Default}, _From, Config) -> + {reply, fix_lookup_result(ets:match(?MODULE, Key), Default), Config}; + +handle_call(all, _From, Config) -> + {reply, lists:sort(ets:tab2list(?MODULE)), Config}; + +%% @doc See register/2 +handle_call({register, Fun, Pid}, _From, #config{notify_funs=PidFuns}=Config) -> + erlang:monitor(process, Pid), + {reply, ok, Config#config{notify_funs=[{Pid, Fun}|PidFuns]}}. + + +fix_lookup_result([{_Key, Value}], _Default) -> + Value; +fix_lookup_result([], Default) -> + Default; +fix_lookup_result(Values, _Default) -> + [list_to_tuple(Value) || Value <- Values]. + +%% @spec insert_and_commit(Tab::etstable(), Config::any()) -> ok +%% @doc Inserts a Key/Value pair into the ets table, writes it to the storage +%% ini file and calls all registered callback functions for Key. +insert_and_commit(Config, KV) -> + true = ets:insert(?MODULE, KV), + % notify funs + %[catch Fun(KV) || {_Pid, Fun} <- Config#config.notify_funs], + couch_config_writer:save_to_file(KV, Config#config.writeback_filename). + +%% @spec load_ini_file(IniFile::filename()) -> ok +%% @doc Parses an ini file and stores Key/Value Pairs into the ets table. +load_ini_file(IniFile) -> + IniFilename = couch_util:abs_pathname(IniFile), + IniBin = + case file:read_file(IniFilename) of + {ok, IniBin0} -> + IniBin0; + {error, enoent} -> + Msg = io_lib:format("Couldn't find server configuration file ~s.", [IniFilename]), + io:format("~s~n", [Msg]), + throw({startup_error, Msg}) + end, + + {ok, Lines} = regexp:split(binary_to_list(IniBin), "\r\n|\n|\r|\032"), + {_, ParsedIniValues} = + lists:foldl(fun(Line, {AccSectionName, AccValues}) -> + case string:strip(Line) of + "[" ++ Rest -> + case regexp:split(Rest, "\\]") of + {ok, [NewSectionName, ""]} -> + {NewSectionName, AccValues}; + _Else -> % end bracket not at end, ignore this line + {AccSectionName, AccValues} + end; + ";" ++ _Comment -> + {AccSectionName, AccValues}; + Line2 -> + case regexp:split(Line2, "=") of + {ok, [_SingleElement]} -> % no "=" found, ignore this line + {AccSectionName, AccValues}; + {ok, [""|_LineValues]} -> % line begins with "=", ignore + {AccSectionName, AccValues}; + {ok, [ValueName|LineValues]} -> % yeehaw, got a line! + RemainingLine = couch_util:implode(LineValues, "="), + {ok, [LineValue | _Rest]} = regexp:split(RemainingLine, " ;|\t;"), % removes comments + {AccSectionName, [{{AccSectionName, ValueName}, LineValue} | AccValues]} + end + end + end, {"", []}, Lines), + + [ets:insert(?MODULE, {Key, Value}) || {Key, Value} <- ParsedIniValues], + ok. + +% Unused gen_server behaviour API functions that we need to declare. + +%% @doc Unused +handle_cast(foo, State) -> {noreply, State}. + + +handle_info({'DOWN', _, _, DownPid, _}, #config{notify_funs=PidFuns}=Config) -> + % remove any funs registered by the downed process + FilteredPidFuns = [{Pid,Fun} || {Pid,Fun} <- PidFuns, Pid /= DownPid], + {noreply, Config#config{notify_funs=FilteredPidFuns}}. + +%% @doc Unused +terminate(_Reason, _State) -> ok. + +%% @doc Unused +code_change(_OldVersion, State, _Extra) -> {ok, State}.
\ No newline at end of file diff --git a/src/couchdb/couch_config_writer.erl b/src/couchdb/couch_config_writer.erl new file mode 100644 index 00000000..6e93c131 --- /dev/null +++ b/src/couchdb/couch_config_writer.erl @@ -0,0 +1,147 @@ +% 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. + +%% @doc Saves a Key/Value pair to a ini file. The Key consists of a Module +%% and Variable combination. If that combination is found in the ini file +%% the new value replaces the old value. If only the Module is found the +%% Variable and value combination is appended to the Module. If the Module +%% does not yet exist in the ini file, it is added and the Variable/Value +%% pair is appended. +%% @see couch_config + +-module(couch_config_writer). +-include("couch_db.hrl"). + +-export([save_to_file/2]). + +%% @spec save_to_file( +%% Config::{{Module::string(), Variable::string()}, Value::string()}, +%% File::filename()) -> ok +%% @doc Saves a Module/Key/Value triple to the ini file File::filename() +save_to_file({{Module, Variable}, Value}, File) -> + + ?LOG_DEBUG("saving to file '~s', Congif: '~p'", [File, {{Module, Variable}, Value}]), + + % open file and create a list of lines + {ok, Stream} = file:read_file(File), + OldFileContents = binary_to_list(Stream), + {ok, Lines} = regexp:split(OldFileContents, "\r\n|\n|\r|\032"), + + % prepare input variables + ModuleName = "[" ++ Module ++ "]", + VariableList = Variable, + + % produce the contents for the config file + NewFileContents = + case NewFileContents2 = save_loop({{ModuleName, VariableList}, Value}, Lines, "", "", []) of + % we didn't change anything, that means we couldn't find a matching + % [ini section] in which case we just append a new one. + OldFileContents -> + append_new_ini_section({{ModuleName, VariableList}, Value}, OldFileContents); + _ -> + NewFileContents2 + end, + + % do the save, close the config file and get out + save_file(File, NewFileContents), + file:close(Stream), + ok. + +%% @doc Iterates over the lines of an ini file and replaces or adds a new +%% configuration directive. +save_loop({{Module, Variable}, Value}, [Line|Rest], OldCurrentModule, Contents, DoneVariables) -> + + % if we find a new [ini section] (Module), save that for reference + NewCurrentModule = parse_module(Line, OldCurrentModule), + + % if the current Module is the one we want to change, try to match + % each line with the Variable + NewContents = case Module of + NewCurrentModule -> + % see if the current line matches the variable we want to substitute + case parse_variable(Line, Variable, Value) of + % nope, return original line + nomatch -> + DoneVariables2 = DoneVariables, + Line; + % got em! return new line + NewLine -> + DoneVariables2 = [Variable|DoneVariables], + NewLine + end; + % if the variable we want to change couldn't be replaced, we append it + % in the proper module section + OldCurrentModule -> + case lists:member(Variable, DoneVariables) of + false -> + DoneVariables2 = [Variable|DoneVariables], + Variable ++ "=" ++ Value ++ "\n" ++ Line; + true -> + DoneVariables2 = DoneVariables, + Line + end; + % otherwise we just print out the original line + _ -> + DoneVariables2 = DoneVariables, + Line + end, + % clumsy way to only append a newline character + % if the line is not empty. We need this to not + % avoid haveing a newline inserted at the top + % of the target file each time we save it. + Contents2 = case Contents of "" -> ""; _ -> Contents ++ "\n" end, + + % go to next line + save_loop({{Module, Variable}, Value}, Rest, NewCurrentModule, Contents2 ++ NewContents, DoneVariables2); + +save_loop(_Config, [], _OldModule, NewFileContents, _DoneVariable) -> + % we're out of new lines, just return the new file's contents + NewFileContents. + +append_new_ini_section({{ModuleName, Variable}, Value}, OldFileContents) -> + OldFileContents ++ "\n\n" ++ ModuleName ++ "\n" ++ Variable ++ "=" ++ Value ++ "\n". + +%% @spec parse_module(Lins::string(), OldModule::string()) -> string() +%% @doc Tries to match a line against a pattern specifying a ini module or +%% section ("[Module]"). Returns OldModule if no match is found. +parse_module(Line, OldModule) -> + case regexp:match(Line, "^\\[([a-zA-Z0-9_-]*)\\]$") of + nomatch -> + OldModule; + {error, Error} -> + io:format("ini file regex error module: '~s'~n", [Error]), + OldModule; + {match, Start, Length} -> + string:substr(Line, Start, Length) + end. + +%% @spec parse_variable(Line::string(), Variable::string(), Value::string()) -> +%% string() | nomatch +%% @doc Tries to match a variable assignment in Line. Returns nomatch if the +%% Variable is not found. Returns a new line composed of the Variable and +%% Value otherwise. +parse_variable(Line, Variable, Value) -> + case regexp:match(Line, "^" ++ Variable ++ "=") of + nomatch -> + nomatch; + {error, Error}-> + io:format("ini file regex error variable: '~s'~n", [Error]), + nomatch; + {match, _Start, _Length} -> + Variable ++ "=" ++ Value + end. + +%% @spec save_file(File::filename(), Contents::string()) -> +%% ok | {error, Reason::string()} +%% @doc Writes Contents to File +save_file(File, Contents) -> + file:write_file(File, list_to_binary(Contents)).
\ No newline at end of file diff --git a/src/couchdb/couch_db_update_notifier_sup.erl b/src/couchdb/couch_db_update_notifier_sup.erl new file mode 100644 index 00000000..8902498d --- /dev/null +++ b/src/couchdb/couch_db_update_notifier_sup.erl @@ -0,0 +1,50 @@ +% 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. + +% +% This causes an OS process to spawned and it is notified every time a database +% is updated. +% +% The notifications are in the form of a the database name sent as a line of +% text to the OS processes stdout. +% + +-module(couch_db_update_notifier_sup). + +-behaviour(supervisor). + +-export([start_link/0,init/1]). + +start_link() -> + supervisor:start_link({local, couch_db_update_notifier_sup}, + couch_db_update_notifier_sup, []). + +init([]) -> + Self = self(), + ok = couch_config:register( + fun({"Update Notification", _}) -> + exit(Self, reload_config) + end), + + UpdateNotifierExes = couch_config:lookup_match( + {{"Update Notification", '$1'}, '$2'}, []), + + {ok, + {{one_for_one, 10, 3600}, + lists:map(fun({Name, UpdateNotifierExe}) -> + {Name, + {couch_db_update_notifier, start_link, [UpdateNotifierExe]}, + permanent, + 1000, + supervisor, + [couch_db_update_notifier]} + end, UpdateNotifierExes)}}.
\ No newline at end of file |