]> granicus.if.org Git - ejabberd/commitdiff
* src/ejabberd_commands.erl: New 'ejabberd commands': separate
authorBadlop <badlop@process-one.net>
Sun, 12 Oct 2008 11:53:25 +0000 (11:53 +0000)
committerBadlop <badlop@process-one.net>
Sun, 12 Oct 2008 11:53:25 +0000 (11:53 +0000)
command definition and calling interface (EJAB-694)
* src/ejabberd_commands.hrl: Likewise

SVN Revision: 1633

ChangeLog
src/ejabberd_commands.erl [new file with mode: 0644]
src/ejabberd_commands.hrl [new file with mode: 0644]

index ecbe9cf747fac24d344b6a0752a4fc4ed3870a84..c7c78b5d0de983c3d38b34a46b33781142da66ab 100644 (file)
--- 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 (file)
index 0000000..0388748
--- /dev/null
@@ -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 (file)
index 0000000..ce7aadb
--- /dev/null
@@ -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.