Bear Section 1.4 in mind, a new way to develop eBox modules have been developed. It relies on Model-View-Controller design pattern, which an architectural pattern used in large software projects to decouple highly data (model) and user interface (view) so that developer can change the data architecture wihout changing the user interface and vice versa. A new component is added, the controller, which manages data access and business logic acting as an intermediate between them.
Thus with our new architecture the eBox MVC conception will be the following:
Our model is defined as a perl module which is a subclass of
EBox::Model::DataTable
or one of the subclasses.
The already defined Mason templates are used to show the information to the user.
The CGIs which are mapped from the Mason templates to do actions. There are some default actions already implemented.
In Chapter 7, there is a large explanation about how it works internally. Below we are only going to introduce the main components which an eBox developer requires to know to build up an eBox MVC's module.
A basic MVC based module should have, at least, these three main parts:
A series of data models which represent the information you want to store to configure your desired service.
Manage your service daemon (if any) accessing to the data model, add menu entry, summary and make the data contained in the model(s) available to the eBox system.
Expose the desired API to other eBox modules, perl scripts and SOAP interface.
A model is just a table concept, that is, an array of rows which have a defined fixed
definition, the table description. This table description contains a series of
EBox::Types
which are instances of EBox::Types::Abstract
,
these eBox types try to include the required representation for eBox configuration parameters,
a brief list could be:
Basic types include are: Boolean
, Int
,
Text
, Password
,
Select
(an enumeration) or Link
, which
describe a type that stores a simple link reference.
These comprise IPAddr
which represents an IP address,
MACAddr
which is a Ethernet address, Service
which matches with port and protocol such as /etc/services
does
and PortRange
which describes a single port number,
a port range or any port.
Currently, just a Union
type has been developed which
represents a type is used from a defined list of options. If none of options are suitable,
a Union::Text
may be used with this purpose.
The data storage is managed automatically by the model itself. A model stores its information
under a GConf directory which must set explicitly at the model creation. However, you can set
dynamically which becomes the model instance in a submodel. The submodels are model instances
whose GConf directory is set dynamically, not only at the runtime. Its usage is intimately with
EBox::Types::HasMany
, which describes a relationship between a field from
a model with another model indicating that field contains the whole data from another model.
Apart from the table description, it is required to set several attributes such as the name, the printable name, if it is ordered, etc... Some model definition examples may help to understand the subject:
Example 1.1. Object
model definition
sub _table { my @tableHead = ( new EBox::Types::Text ( 'fieldName' => 'name', 'printableName' => __('Name'), 'size' => '12', 'unique' => 1, 'editable' => 1 ), new EBox::Types::HasMany ( 'fieldName' => 'members', 'printableName' => __('Members'), 'foreignModel' => 'MemberTable', 'view' => '/ebox/Object/View/MemberTable', 'backView' => '/ebox/Object/View/MemberTable', 'size' => '1', ) ); my $dataTable = { 'tableName' => 'ObjectTable', 'printableTableName' => __('Objects'), 'automaticRemove' => 1, 'modelDomain' => 'Objects', 'defaultActions' => ['add', 'del', 'editField' ], 'tableDescription' => \@tableHead, 'class' => 'dataTable', 'help' => __('Objects'), 'printableRowName' => __('object'), }; return $dataTable; }
As it is shown, a model is used to describe an eBox object, which has a name and a group of members which are described in Example 1.2:
Example 1.2. Object member
model definition
sub _table { my @tableHead = ( new EBox::Types::Text ( 'fieldName' => 'name', 'printableName' => __('Name'), 'size' => '12', 'unique' => 1, 'editable' => 1 ), new EBox::Types::IPAddr ( 'fieldName' => 'ipaddr', 'printableName' => __('IP Address'), 'editable' => 1, ), new EBox::Types::MACAddr ( 'fieldName' => 'macaddr', 'printableName' => __('MAC Address'), 'editable' => 1, 'optional' => 1 ), ); my $dataTable = { 'tableName' => 'MemberTable', 'printableTableName' => __('Members'), 'automaticRemove' => 1, 'defaultActions' => ['add', 'del', 'editField' ], 'modelDomain' => 'Objects', 'tableDescription' => \@tableHead, 'class' => 'dataTable', 'help' => __('Objects'), 'printableRowName' => __('member'), }; return $dataTable; }
As you may see, every field has some attributes as well as the model itself. Just note the actions are the allowed actions to apply to that model. For instance, you can add, delete rows, edit fields... Several attributes are allowed for every type and model, if they are not explicitly set at the model definition, they are treated as false value. That is, if it is a boolean type or empty string if the type is an string.
The best part of it comes from forms. Typical eBox scenarios contain a set of tables and forms. So the information that a form may show can be stored in a data model as described above. However, it has some significant differences. A form data model looks like a table data model with just one row which is always shown to be edited. A form model definition is similar to a table data model one as Example 1.3 depicts:
Example 1.3. Jabber dispatcher
configuration form data model
definition
sub _table { my @tableDesc = ( new EBox::Types::Text( fieldName => 'server', printableName => __('Jabber server name'), size => 12, editable => 1, ), new EBox::Types::Int( fieldName => 'port', printableName => __('Port'), size => 6, editable => 1, defaultValue => 5222, ), new EBox::Types::Text( fieldName => 'user', printableName => __('Jabber user name'), size => 12, editable => 1, ), new EBox::Types::Password( fieldName => 'password', printableName => __('User password'), size => 12, editable => 1, minLength => 4, maxLength => 25, ), new EBox::Types::Boolean( fieldName => 'subscribe', printableName => __('Subscribe'), editable => 1, ), new EBox::Types::Text( fieldName => 'adminJID', printableName => __('Administrator Jabber Identifier'), size => 12, editable => 1, ), ); my $dataForm = { tableName => 'JabberDispatcherForm', printableTableName => __('Configure Jabber dispatcher'), modelDomain => 'Events', tableDescription => \@tableDesc, class => 'dataForm', help => __('In order to configure the Jabber event dispatcher ' . 'is required to be registered at the chosen Jabber ' . 'server or check subscribe to do register. The administrator ' . 'identifier should follow the pattern: user@domain[/resource]'), }; return $dataForm; }
As you may notice, fields at a form can contain default values. This is done due to have default eBox configuration working. For example, you may want to enable a service to a specific port whose value does not modify the service if the default value is set.
Once the definition is set you may want to validate the user input. Types do some
validation, for instance, IPAddr
checks if the parameters
contain a valid IP. However, there are cases where the developer requires to do
extra validation. To achieve so, there are two methods to override
validateRow
and validateTypedRow
.
Furthermore, actions have callbacks to called after an action have been finished.
Therefore add action has addedRowNotify
,
edit updatedRowNotify
and so on. This
allows you to manage the updated information to do something different from just
storing updating information at GConf database.
This section explains how the data model is shown at eBox user interface. If you just want to show a data model in eBox, a new menu entry is required. Its URL must have this pattern: ${modelDomain}/View/${tableName}. The modelDomain and tableName are attributes which have been already set at the data model definition. A table model will show up an empty table and a HTML form will appear if a form model is defined, they are named as their viewers.
You may wish to combine the several models in the same page. Here where the model composites play its role. The model composites are containers which stores two kinds of components: models and composites, thus you can create complex composites to find the more usable and adapted user interface for your needs. The current available layouts are:
The components are shown in order from top to the bottom.
The component viewers are displayed in a tabbed way. That is, each component is shown when the tab name is clicked
Define a model composite is even simpler than a model one as it depicts in this example:
Example 1.4. A sample model composite definition
sub _description { my $description = { components => [ 'EnableForm', 'ConfigurationComposite', ], layout => 'top-bottom', name => 'GeneralComposite', printableName => __('Events'), compositeDomain => 'Events', help => __('Events module may help you to make eBox ' . 'inform you about events that happen at eBox ' . 'in some different ways'), }; return $description; }
The components are referenced using its name, tableName at models and name at composites. In order to display a composite based page, it is required to change the menu entry. In this case, the URL to call must follow this pattern ${compositeDomain}/Composite/${name}. That's all you need to display your models together in a single page.
Just get the magic done, it is required to publish the models and composites.
To achieve so, the eBox modules, which are subclasses of
EBox::GConfModule
, must inherit from
EBox::Model::ModelProvider
and
EBox::Model::CompositeProvider
interfaces, implementing
its respectively methods models
and
composites
, which returns an array reference of model and
composite instances that lives within this eBox module realm. An example could be
the next one:
Example 1.5. Model and composite publishing example
sub models { my ($self) = @_; return [ $self->configureLogModel(), $self->forcePurgeModel(), ]; } sub composites { my ($self) = @_; return [ $self->_configureLogComposite(), ]; }
Once the models are published and ready to be displayed, they actually have to do something to manage its service. Current data model API is quite experimental in the way it is constantly evolving to satisfy the client needs. The main components that use data model are migration scripts and eBox module itself to obtain the required data to configure the service.
The main method to retrieve the data model is rows
which
returns an array reference which contains the rows from that data model. You can filter
the result using a filter
string or get the result from
a defined page
, since data model allows filtering and paging. Each
array component contains the return value from row
which is a
hash reference containing the following key - value pairs:
Each row has an identifier which allows distinguishing from the others
inside the data model. You can use it as well as indexer calling the
row
method.
A data model can be ordered or not. If so, the rows are ordered in the same
order they have been added. Furthermore, you might tailor the order overriding
_tailoredOrder
to give rows in the desired order.
Array reference which contains eBox types, which were explained previously.
Hash reference indexed by field name whose values are eBox types as values does.
For convenience, some methods have been added to help accessing the methods. This one returns a hash reference containing field name - value pairs.
Similar to the previous one, it returns a hash reference containing field name - printable value pairs.
The rows
may be quite complex. An dumped from a result
could clarify the subject:
Example 1.6. Rows
dumped result
[ { 'order' => undef, 'plainValueHash' => { 'members' => { ... }, 'name' => 'RRHH', 'id' => 'x6666' }, 'valueHash' => { 'members' => bless( { ... }, 'EBox::Types::HasMany' ), 'name' => bless( { 'unique' => 1, 'value' => 'RRHH', 'printableName' => 'Name', 'model' => $VAR1->[0]{'valueHash'}{'members'}{'model'}, 'HTMLSetter' => '/ajax/setter/textSetter.mas', 'optional' => 0, 'size' => '12', 'row' => $VAR1->[0], 'editable' => 1, 'type' => 'text', 'HTMLViewer' => '/ajax/viewer/textViewer.mas', 'fieldName' => 'name' }, 'EBox::Types::Text' ) }, 'values' => [ $VAR1->[0]{'valueHash'}{'name'}, $VAR1->[0]{'valueHash'}{'members'} ], 'id' => 'x6666', 'printableValueHash' => { 'members' => { 'model' => 'MemberTable', 'values' => [ { 'macaddr' => undef, 'name' => 'Claudia', 'id' => 'memb44', 'ipaddr' => '192.168.1.92/32' } ], 'directory' => 'objectTable/keys/x6666/members' }, 'name' => 'RRHH', 'id' => 'x6666' }, 'readOnly' => 0 }, ];
With the ease of use in mind, form data model allows accessing directly to the value,
printable value attributes or the whole type using as method name the field name. For instance,
at jabber dispatcher configuration form data model,
you can access to the value, printable value and the instanced type itself of
user property calling userValue
,
userPrintableValue
and userType
respectively.
Nevertheless, you may search for rows which match a fixed criterion, this implies the
match is done against the field value. Four methods make the
work for you, find
, findAll
,
findValue
and findValueAll
. They are divided
in two groups, the ones which returns the valueHash and the ones
which returns the printableValueHash and has the 'Value' word in their names.
Moreover, if the method ends with 'All' words, returns not just the first row which matchs
but also the possible remainder rows which match the criterion as well. Taking the previous
data model a search with $model->find( 'name' => 'RHHH' )
, the dumped return value
is:
{ 'members' => { 'model' => 'MemberTable', 'values' => [ { 'macaddr' => undef, 'name' => 'Claudia', 'id' => 'memb44', 'ipaddr' => '192.168.1.92/32' } ], 'directory' => 'objectTable/keys/x6666/members' }, 'name' => 'RRHH', 'id' => 'x6666' }
Final part of developing with MVC framework is expose the desired API to other eBox modules, perl scripts and SOAP clients. Two main options are probable, just disclose the instanced data model allowing full access to the model using in the way we used above or integrate some helper methods to access to the most important features which clients use most. The latter is likely the chosen if the data model rises in its complexity. A module which consists of several data models connected with each other indicates that a simple API may help the module's clients.
As you may see, the MVC framework is powerful scheme to develop in eBox. However, it is not so impressive yet. It is still under development and some optimization as well as lack of some features. For instance, an eBox type to manage file uploading from the user, automatic getter and setter methods,...