1.5. A Model-View-Controller module

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:

Model

Our model is defined as a perl module which is a subclass of EBox::Model::DataTable or one of the subclasses.

View

The already defined Mason templates are used to show the information to the user.

Controller

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:

1.5.1. Model creation

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

Basic types include are: Boolean, Int, Text, Password, Select (an enumeration) or Link, which describe a type that stores a simple link reference.

Network related types

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.

Composite types

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 dispatcherconfiguration 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.

1.5.1.1. User input management

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.

1.5.2. Model view

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:

top-bottom

The components are shown in order from top to the bottom.

tabbed

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.

1.5.3. Data model publishing and access

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:

id

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.

order

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.

values

Array reference which contains eBox types, which were explained previously.

valueHash

Hash reference indexed by field name whose values are eBox types as values does.

plainValueHash

For convenience, some methods have been added to help accessing the methods. This one returns a hash reference containing field name - value pairs.

printableValueHash

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.

1.5.4. When is this approach suitable?

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,...