aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBadlop <badlop@process-one.net>2008-10-12 11:53:25 +0000
committerBadlop <badlop@process-one.net>2008-10-12 11:53:25 +0000
commitdda582db13bf9245edcddc1e3c2e1bd0bf6c8b67 (patch)
treeebef3a09c4206ad96f2221256c9a767e003ca67d
parent* src/mod_proxy65/mod_proxy65.erl: Update so the listener starts (diff)
* src/ejabberd_commands.erl: New 'ejabberd commands': separate
command definition and calling interface (EJAB-694) * src/ejabberd_commands.hrl: Likewise SVN Revision: 1633
-rw-r--r--ChangeLog4
-rw-r--r--src/ejabberd_commands.erl328
-rw-r--r--src/ejabberd_commands.hrl52
3 files changed, 384 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index ecbe9cf74..c7c78b5d0 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
2008-10-12 Badlop <badlop@process-one.net>
+ * src/ejabberd_commands.erl: New 'ejabberd commands': separate
+ command definition and calling interface (EJAB-694)
+ * src/ejabberd_commands.hrl: Likewise
+
* src/mod_proxy65/mod_proxy65.erl: Update so the listener starts
correctly (EJAB-303)
* src/mod_proxy65/mod_proxy65_service.erl: Likewise
diff --git a/src/ejabberd_commands.erl b/src/ejabberd_commands.erl
new file mode 100644
index 000000000..0388748ff
--- /dev/null
+++ b/src/ejabberd_commands.erl
@@ -0,0 +1,328 @@
+%%%----------------------------------------------------------------------
+%%% File : ejabberd_commands.erl
+%%% Author : Badlop <badlop@process-one.net>
+%%% Purpose : Management of ejabberd commands
+%%% Created : 20 May 2008 by Badlop <badlop@process-one.net>
+%%%
+%%%
+%%% ejabberd, Copyright (C) 2002-2008 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+%%% @headerfile "ejabberd_commands.hrl"
+
+%%% @doc Management of ejabberd commands.
+%%%
+%%% An ejabberd command is an abstract function identified by a name,
+%%% with a defined number and type of calling arguments and type of
+%%% result, that can be defined in any Erlang module and executed
+%%% using any valid frontend.
+%%%
+%%%
+%%% == Define a new ejabberd command ==
+%%%
+%%% ejabberd commands can be defined and registered in
+%%% any Erlang module.
+%%%
+%%% Some commands are procedures; and their purpose is to perform an
+%%% action in the server, so the command result is only some result
+%%% code or result tuple. Other commands are inspectors, and their
+%%% purpose is to gather some information about the server and return
+%%% a detailed response: it can be integer, string, atom, tuple, list
+%%% or a mix of those ones.
+%%%
+%%% The arguments and result of an ejabberd command are strictly
+%%% defined. The number and format of the arguments provided when
+%%% calling an ejabberd command must match the definition of that
+%%% command. The format of the result provided by an ejabberd command
+%%% must be exactly its definition. For example, if a command is said
+%%% to return an integer, it must always return an integer (except in
+%%% case of a crash).
+%%%
+%%% If you are developing an Erlang module that will run inside
+%%% ejabberd and you want to provide a new ejabberd command to
+%%% administer some task related to your module, you only need to:
+%%% implement a function, define the command, and register it.
+%%%
+%%%
+%%% === Define a new ejabberd command ===
+%%%
+%%% An ejabberd command is defined using the Erlang record
+%%% 'ejabberd_commands'. This record has several elements that you
+%%% must define. Note that 'tags', 'desc' and 'longdesc' are optional.
+%%%
+%%% For example let's define an ejabberd command 'pow' that gets the
+%%% integers 'base' and 'exponent'. Its result will be an integer
+%%% 'power':
+%%%
+%%% <pre>#ejabberd_commands{name = pow, tags = [test],
+%%% desc = "Return the power of base for exponent",
+%%% longdesc = "This is an example command. The formula is:\n"
+%%% " power = base ^ exponent",
+%%% module = ?MODULE, function = pow,
+%%% args = [{base, integer}, {exponent, integer}],
+%%% result = {power, integer}}</pre>
+%%%
+%%%
+%%% === Implement the function associated to the command ===
+%%%
+%%% Now implement a function in your module that matches the arguments
+%%% and result of the ejabberd command.
+%%%
+%%% For example the function calc_power gets two integers Base and
+%%% Exponent. It calculates the power and rounds to an integer:
+%%%
+%%% <pre>calc_power(Base, Exponent) ->
+%%% PowFloat = math:pow(Base, Exponent),
+%%% round(PowFloat).</pre>
+%%%
+%%% Since this function will be called by ejabberd_commands, it must be exported.
+%%% Add to your module:
+%%% <pre>-export([calc_power/2]).</pre>
+%%%
+%%% Only some types of result formats are allowed.
+%%% If the format is defined as 'rescode', then your function must return:
+%%% ok | true | atom()
+%%% where the atoms ok and true as considered positive answers,
+%%% and any other response atom is considered negative.
+%%%
+%%% If the format is defined as 'restuple', then the command must return:
+%%% {rescode(), string()}
+%%%
+%%% If the format is defined as '{list, something()}', then the command
+%%% must return a list of something().
+%%%
+%%%
+%%% === Register the command ===
+%%%
+%%% Define this function and put inside the #ejabberd_command you
+%%% defined in the beginning:
+%%%
+%%% <pre>commands() ->
+%%% [
+%%%
+%%% ].</pre>
+%%%
+%%% You need to include this header file in order to use the record:
+%%%
+%%% <pre>-include("ejabberd_commands.hrl").</pre>
+%%%
+%%% When your module is initialized or started, register your commands:
+%%%
+%%% <pre>ejabberd_commands:register_commands(commands()),</pre>
+%%%
+%%% And when your module is stopped, unregister your commands:
+%%%
+%%% <pre>ejabberd_commands:unregister_commands(commands()),</pre>
+%%%
+%%% That's all! Now when your module is started, the command will be
+%%% registered and any frontend can access it. For example:
+%%%
+%%% <pre>$ ejabberdctl help pow
+%%%
+%%% Command Name: pow
+%%%
+%%% Arguments: base::integer
+%%% exponent::integer
+%%%
+%%% Returns: power::integer
+%%%
+%%% Tags: test
+%%%
+%%% Description: Return the power of base for exponent
+%%%
+%%% This is an example command. The formula is:
+%%% power = base ^ exponent
+%%%
+%%% $ ejabberdctl pow 3 4
+%%% 81
+%%% </pre>
+%%%
+%%%
+%%% == Execute an ejabberd command ==
+%%%
+%%% ejabberd commands are mean to be executed using any valid
+%%% frontend. An ejabberd commands is implemented in a regular Erlang
+%%% function, so it is also possible to execute this function in any
+%%% Erlang module, without dealing with the associated ejabberd
+%%% commands.
+%%%
+%%%
+%%% == Frontend to ejabberd commands ==
+%%%
+%%% Currently there are two frontends to ejabberd commands: the shell
+%%% script {@link ejabberd_ctl. ejabberdctl}, and the XML-RPC server
+%%% mod_xmlrpc.
+%%%
+%%%
+%%% === ejabberdctl as a frontend to ejabberd commands ===
+%%%
+%%% It is possible to use ejabberdctl to get documentation of any
+%%% command. But ejabberdctl does not support all the argument types
+%%% allowed in ejabberd commands, so there are some ejabberd commands
+%%% that cannot be executed using ejabberdctl.
+%%%
+%%% Also note that the ejabberdctl shell administration script also
+%%% manages ejabberdctl commands, which are unrelated to ejabberd
+%%% commands and can only be executed using ejabberdctl.
+%%%
+%%%
+%%% === ejabberd_xmlrpc as a frontend to ejabberd commands ===
+%%%
+%%% ejabberd_xmlrpc provides an XML-RPC server to execute ejabberd commands.
+%%% ejabberd_xmlrpc is a contributed module published in ejabberd-modules SVN.
+%%%
+%%% Since ejabberd_xmlrpc does not provide any method to get documentation
+%%% of the ejabberd commands, please use ejabberdctl to know which
+%%% commands are available, and their usage.
+%%%
+%%% The number and format of the arguments provided when calling an
+%%% ejabberd command must match the definition of that command. Please
+%%% make sure the XML-RPC call provides the required arguments, with
+%%% the specified format. The order of the arguments in an XML-RPC
+%%% call is not important, because all the data is tagged and will be
+%%% correctly prepared by mod_xmlrpc before executing the ejabberd
+%%% command.
+
+%%% TODO: consider this feature:
+%%% All commands are catched. If an error happens, return the restuple:
+%%% {error, flattened error string}
+%%% This means that ecomm call APIs (ejabberd_ctl, ejabberd_xmlrpc) need to allows this.
+%%% And ejabberd_xmlrpc must be prepared to handle such an unexpected response.
+
+
+-module(ejabberd_commands).
+-author('badlop@process-one.net').
+
+-export([init/0,
+ list_commands/0,
+ get_command_format/1,
+ get_command_definition/1,
+ get_tags_commands/0,
+ register_commands/1,
+ unregister_commands/1,
+ execute_command/2
+ ]).
+
+-include("ejabberd_commands.hrl").
+-include("ejabberd.hrl").
+
+
+init() ->
+ ets:new(ejabberd_commands, [named_table, set, public,
+ {keypos, #ejabberd_commands.name}]).
+
+%% @spec ([ejabberd_commands()]) -> ok
+%% @doc Register ejabberd commands.
+%% If a command is already registered, a warning is printed and the old command is preserved.
+register_commands(Commands) ->
+ lists:foreach(
+ fun(Command) ->
+ case ets:insert_new(ejabberd_commands, Command) of
+ true ->
+ ok;
+ false ->
+ ?WARNING_MSG("This command is already defined:~n~p", [Command])
+ end
+ end,
+ Commands).
+
+%% @spec ([ejabberd_commands()]) -> ok
+%% @doc Unregister ejabberd commands.
+unregister_commands(Commands) ->
+ lists:foreach(
+ fun(Command) ->
+ ets:delete_object(ejabberd_commands, Command)
+ end,
+ Commands).
+
+%% @spec () -> [{Name::atom(), Args::[aterm()], Desc::string()}]
+%% @doc Get a list of all the available commands, arguments and description.
+list_commands() ->
+ Commands = ets:match(ejabberd_commands,
+ #ejabberd_commands{name = '$1',
+ args = '$2',
+ desc = '$3',
+ _ = '_'}),
+ [{A, B, C} || [A, B, C] <- Commands].
+
+%% @spec (Name::atom()) -> {Args::[aterm()], Result::rterm()} | {error, command_unknown}
+%% @doc Get the format of arguments and result of a command.
+get_command_format(Name) ->
+ Matched = ets:match(ejabberd_commands,
+ #ejabberd_commands{name = Name,
+ args = '$1',
+ result = '$2',
+ _ = '_'}),
+ case Matched of
+ [] ->
+ {error, command_unknown};
+ [[Args, Result]] ->
+ {Args, Result}
+ end.
+
+%% @spec (Name::atom()) -> ejabberd_commands() | command_not_found
+%% @doc Get the definition record of a command.
+get_command_definition(Name) ->
+ case ets:lookup(ejabberd_commands, Name) of
+ [E] -> E;
+ [] -> command_not_found
+ end.
+
+%% @spec (Name::atom(), Arguments) -> ResultTerm | {error, command_unknown}
+%% @doc Execute a command.
+execute_command(Name, Arguments) ->
+ case ets:lookup(ejabberd_commands, Name) of
+ [Command] ->
+ execute_command2(Command, Arguments);
+ [] ->
+ {error, command_unknown}
+ end.
+
+execute_command2(Command, Arguments) ->
+ Module = Command#ejabberd_commands.module,
+ Function = Command#ejabberd_commands.function,
+ apply(Module, Function, Arguments).
+
+%% @spec () -> [{Tag::string(), [CommandName::string()]}]
+%% @doc Get all the tags and associated commands.
+get_tags_commands() ->
+ CommandTags = ets:match(ejabberd_commands,
+ #ejabberd_commands{
+ name = '$1',
+ tags = '$2',
+ _ = '_'}),
+ Dict = lists:foldl(
+ fun([CommandNameAtom, CTags], D) ->
+ CommandName = atom_to_list(CommandNameAtom),
+ case CTags of
+ [] ->
+ orddict:append("untagged", CommandName, D);
+ _ ->
+ lists:foldl(
+ fun(TagAtom, DD) ->
+ Tag = atom_to_list(TagAtom),
+ orddict:append(Tag, CommandName, DD)
+ end,
+ D,
+ CTags)
+ end
+ end,
+ orddict:new(),
+ CommandTags),
+ orddict:to_list(Dict).
diff --git a/src/ejabberd_commands.hrl b/src/ejabberd_commands.hrl
new file mode 100644
index 000000000..ce7aadb9b
--- /dev/null
+++ b/src/ejabberd_commands.hrl
@@ -0,0 +1,52 @@
+%%%----------------------------------------------------------------------
+%%%
+%%% ejabberd, Copyright (C) 2002-2008 ProcessOne
+%%%
+%%% This program is free software; you can redistribute it and/or
+%%% modify it under the terms of the GNU General Public License as
+%%% published by the Free Software Foundation; either version 2 of the
+%%% License, or (at your option) any later version.
+%%%
+%%% This program is distributed in the hope that it will be useful,
+%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
+%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+%%% General Public License for more details.
+%%%
+%%% You should have received a copy of the GNU General Public License
+%%% along with this program; if not, write to the Free Software
+%%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+%%% 02111-1307 USA
+%%%
+%%%----------------------------------------------------------------------
+
+-record(ejabberd_commands, {name, tags = [],
+ desc = "", longdesc = "",
+ module, function,
+ args = [], result = rescode}).
+
+%% @type ejabberd_commands() = #ejabberd_commands{
+%% name = atom(),
+%% tags = [atom()],
+%% desc = string(),
+%% longdesc = string(),
+%% module = atom(),
+%% function = atom(),
+%% args = [aterm()],
+%% result = rterm()
+%% }.
+%% desc: Description of the command
+%% args: Describe the accepted arguments.
+%% This way the function that calls the command can format the
+%% arguments before calling.
+
+%% @type atype() = integer | string | {tuple, [aterm()]} | {list, aterm()}.
+%% Allowed types for arguments are integer, string, tuple and list.
+
+%% @type rtype() = integer | string | atom | {tuple, [rterm()]} | {list, rterm()} | rescode | restuple.
+%% A rtype is either an atom or a tuple with two elements.
+
+%% @type aterm() = {Name::atom(), Type::atype()}.
+%% An argument term is a tuple with the term name and the term type.
+
+%% @type rterm() = {Name::atom(), Type::rtype()}.
+%% A result term is a tuple with the term name and the term type.