The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

OpenInteract2::Manage - Provide common functions and factory for management tasks

SYNOPSIS

 # Common programmatic use of management task:
 
 use strict;
 use OpenInteract2::Manage;
 
 my $task = OpenInteract2::Manage->new(
                    'install_package',
                    { filename    => '/home/httpd/site/uploads/file.tar.gz',
                      website_dir => '/home/httpd/site' } );
 my @status = eval { $task->execute };
 if ( $@ ) {
     if ( $@->isa( 'OpenInteract2::Exception::Parameter' ) ) {
         my $failures = $@->parameter_fail;
         while ( my ( $field, $reasons ) = each %{ $failures } ) {
             print "Field $field: ", join( ", ", @{ $reasons } ), "\n";
         }
     }
     exit;
 }
 
 foreach my $s ( @status ) {
     print "Status: ", ( $s->{is_ok} eq 'yes' ) ? 'OK' : 'NOT OK';
     print "\n$s->{message}\n";
 }
 
 # Every task needs to implement the following:
 
 sub run_task         {}
 
 # The task can implement this to initialize the object
 
 sub init             {}
 
 # The task can also implement these for setting up/clearing out the
 # environment
 
 sub setup_task       {}
 sub tear_down_task   {}
 
 # The task can also implement these for checking/validating
 # parameters
 
 sub list_param_required {}
 sub list_param_validate {}
 sub get_validate_sub    {}

DESCRIPTION

OpenInteract2::Manage is the organizer, interface and factory for tasks managing OpenInteract2. Its goal is to make these tasks runnable from anywhere, not just the command-line, and to provide output that can be parsed in a sufficiently generic format to be useful anywhere.

Since it is an organizing module it does not actually perform the tasks. You will want to see OpenInteract2::Manage::Package or OpenInteract2::Manage::Website to get closer to that metal. You can also subclass this class directly, but look first into the other subclasses as they may provide functionality to make your task easier to implement.

Additionally, most people will probably use the oi2_manage front-end to this set of tasks, so you probably want to look there if you're itching to do something quickly.

METHODS

new( $task, [ \%params, ], [ @extra_params ] )

Creates a new management task of type $task. If type $task is not yet registered, the method throws an exception.

You can also pass any number of \%params with which the management task gets initialized (using init(), below). These are blindly set and not checked until you run execute().

All of the extra_params are passed to init(), which subclasses may implement to do any additional initialization.

Returns: New management task object

execute()

Runs through the methods check_parameters(), setup_task(), run_task(), tear_down_task().

Any of these methods can throw an exception, so it is up to you to wrap the call to execute() in an eval block and examine $@.

Returns: an arrayref of status hash references. These should include the keys 'is_ok' (set to 'yes' if the item succeeded, 'no' if not) and 'message' describing the results. Tasks may set additional items as well, all of which should be documented in the task.

You can also retrieve the status messages by calling get_status().

is_valid_task( $task_name )

Returns true if $task_name is a valid task, false if not.

valid_tasks()

Query the class about what tasks are currently registered.

Returns: list of registered tasks

valid_tasks_description()

Query the class about what tasks are currently registered, plus get a brief description of each.

Returns: hashref of registered tasks (keys) and their descriptions (values).

OBSERVERS

Every management task is observable. (See Class::Observable for what this means.) As a creator and user of a task you can add your own observers to it and receive status and progress messages from the task as it performs its work.

There are two types of standard observations posted from management tasks. This type is passed as the first argument to your observer.

  • status: This is a normal status message. (See "STATUS MESSAGES" for what this means.) The second argument passed to your observer will be the hashref representing the status message.

  • progress: Indicates a new stage of the process has been reached or completed. The second argument to your observer is a text message, the optional third argument is a hashref of additional information. Currently this has only one option: long may be set to 'yes', and if so the task is telling you it's about to begin a long-running process.

For an example of an observer, see oi2_manage.

PARAMETERS AND CHECKING

Every management task should be initialized with parameters that tell the task how or where to perform its work. This parent class provides the means to ensure required parameters are defined and that they are valid. This parameter checking is very flexible so it is simple to define your own validation checks and tell them to this parent class.

Access/Modify Parameters

param( $key, $value )

If $key is unspecified, returns all parameters as a hashref.

If $value is unspecified, returns the current value set for parameter $key.

If both $key and $value are specified, sets the parameter $key to $value and returns it.

Example:

 $task->param( 'website_dir', '/home/httpd/test' );
 $task->param( package => [ 'pkg1', 'pkg2' ] );
 my $all_params = $task->param;

Another way of setting parameters is by passing them into the constructor. The second argument (hashref) passed into the new() call can be set to the parameters you want to use for the task. This makes it simple to do initialization and execution in one step:

 my @status = OpenInteract2::Manage->new( 'create_website',
                                          { website_dir  => '/home/httpd/test' } )
                                   ->execute();

Checking Parameters: Flow

The management class has a fairly simple but flexible way for you to ensure that your task gets valid parameters.

First, you can ensure that all the parameters required are defined by the task caller. Simply create a method list_param_required() which returns an arrayref of parameters that require a value to be defined:

 sub list_param_required { return [ 'website_dir', 'package_dir' ] }

You can also override the method check_required_parameters(), but this requires you to throw the exceptions yourself.

Next, you need to ensure that all the parameters are valid. There are a couple of ways to do this

Checking Parameters: Methods

check_parameters()

This method is really just a wrapper for parameter initialization, required parameter checking and parameter validation.

It is called from execute() before run_task() is called. It depends on the methods list_param_required() and list_param_validate() being defined in your task.

The first action it performs is to call param_initialize() so your task can do any necessary parameter manipulation.

Next it calls check_required_parameters(), which cycles through the arrayref returned by list_param_required() and ensures that a value for each parameter exists.

Finally it calls check_valid_parameters(), which ensures that parameters requiring validation (those returned by list_param_validated()) are valid.

Any errors thrown by these methods are percolated up back to the caller. Barring strange runtime errors they're going to be OpenInteract2::Exception::Parameter objects, which means the caller can do a filter as needed, displaying more pertient information:

 eval { $task->execute }
 my $error = $@;;
 if ( $error ) {
     if ( $error->isa( 'OpenInteract2::Exception::Parameter' ) ) {
         print "Caught an exception with one or more paramters:\n";
         my $failed = $error->parameter_fail;
         while ( my ( $field, $fail ) = each %{ $failed } ) {
             my @failures = ( ref $fail eq 'ARRAY' ) ? @{ $fail } : ( $fail );
             foreach my $failure ( @failures ) {
                 print sprintf( "%-20s-> %s\n", $field, $failure );
             }
         }
     }
 }

param_initialize()

This class implements this method to massage the 'package' parameter into a consistent format.

You may want to implement it to modify your parameters before any checking or validation. For instance, tasks dealing with packages typically allow you to pass in a list or a comma-separated string, or even use a keyword to represent multiple packages. The param_initialize() method can change each of these into a consistent format, allowing the task to assume it will always be dealing with an arrayref.

If you're a subclass you should always pass the call up to your parent via SUPER.

check_required_parameters()

Calls list_param_require and ensures that each parameter listed in the returned arrayref has been set in the task. If not, it throws a OpenInteract2::Exception::Parameter error with the name of all undefined parameters.

check_valid_parameters()

Calls list_param_validate and ensures that each parameter listed is valid. What 'valid' means depends on you: one of your parents may implement a validation routine (e.g., OpenInteract2::Manage or OpenInteract2::Manage::Website) or you may implement your own.

You create your own validation routine by returning a subroutine reference from a call to get_validate_sub() which includes the parameter being validated as the argument. This subroutine should return undef if the parameter is valid or an error message if it is not.

See an example of this in SUBCLASSING.

get_validate_sub( $param_name )

Return a parameter subroutine to validate parameter $param_name. The subroutine is passed the task object and the value of parameter $param_name. It should return nothing (undef is ok) if the parameter value is valid. Otherwise it should return an error message.

If you're a subclass you should forward the request onto your parents via SUPER. (See example below.)

STATUS MESSAGES

Status messages are simple hashrefs with at least three entries:

  • is_ok: Set to 'yes' if this a successful status, 'no' if not.

  • action: Name of the action.

  • message: Message describing the action or the error encountered.

Each message may have any number of additional entries. A common one is filename, which is used to indicate the file acted upon. Every management task should list what keys its status messages support, not including the three listed above.

Some tasks can generate a lot of status messages, so the method merge_status_by_action will merge all status messages with the same action into a single message with the keys action (the action) and status (an arrayref of the collected status messages under that action).

SUBCLASSING

The following is for developers creating new management tasks.

Mandatory methods

Management tasks must implement:

run_task()

This is where you actually perform the work of your task. You can indicate the status of your task with status hashrefs passed to _add_status() or _add_status_head(). (See "STATUS MESSAGES" above.)

Errors are indicated by throwing an exception -- generally an OpenInteract2::Exception object, but if you want to create your own there is nothing stopping you.

The task manager will set the parameter task_failed to 'yes' if it catches an error from run_task. This allows you to do conditional cleanup in tear_down_task, discussed below.

Note that the caller ensures that the directory remains the same for the caller, so you can chdir to your heart's content.

Optional methods

init( @extra )

This is called within the new() method. All extra parameters sent to new() are passed to this method, since the main parameters have already been set in the object.

setup_task()

Sets up the environment required for this task. This might require creating an OpenInteract2::Context, a database connection, or some other action. (Some of these have shortcuts -- see below.)

If you cannot setup the required environment you should throw an exception with an appropriate message.

tear_down_task()

If your task needs to do any cleanup actions -- closing a database connection, etc. -- it should perform them here.

The task manager will set the parameter task_failed to 'yes' if the main task threw an error. This allows you to do conditional cleanup -- for instance, OpenInteract2::Manage::Website::Create checks this field and if it is set will remove the directories created and all the files copied in the halted process of creating a new website.

list_param_required()

This should return an arrayref of parameters. Before executing run_task() the parent class will ensure that all parameters specified in the arrayref are defined before continuing.

Parameter Validation

Here's an example where we depend on the validation routine for website_dir from OpenInteract2::Manage:

 sub list_param_validate { return [ 'website_dir' ] }

That's it! check_valid_parameters() will see that you'd like to validate 'website_dir', look for a routine to validate it and fine one.

Now, say we want to validate a different parameter:

 sub list_param_validate { return [ 'game_choice' ] }
 
 sub get_validate_sub {
     my ( $self, $param_name ) = @_;
     if ( $param_name eq 'game_choice' ) {
         return \&_rock_scissors_paper;
     }
     return $self->SUPER::get_validate_sub( $param_name );
 }
 
 sub _rock_scissors_paper {
     my ( $self, $game_choice ) = @_;
     unless ( $game_choice =~ /^(rock|scissors|paper)$/i ) {
         return "Value must be 'rock', 'scissors' or 'paper'";
     }
     return undef;
 }

This ensures that the parameter 'game_choice' is either 'rock', 'scissors' or 'paper' (case-insensitive). your run_task() method will never be run unless all the parameter requirements and validation checks are successful.

Status helper methods

These methods should only be used by management tasks themselves, not by the users of those tasks.

Note: All status messages are sent to the observers as a 'status' observation. These are sent in the order received, so the user may be a little confused if you use _add_status_head().

_add_status( ( \%status, \%status, ...) )

Adds status message \%status to those tracked by the object.

_add_status_head( ( \%status, \%status, ... ) )

Adds status messages to the head of the list of status messages. This is useful for when your management task comprises several others. You can collect their status messages as your own, then insert an overall status as the initial one seen by the user.

Notifying Observers

All management tasks are observable. This means anyone can add any number of classes, objects or subroutines that receive observations you post. Notifying observers is simple:

 $self->notify_observers( $type, @extra_info )

What goes into @extra_info depends on the $type. The two types of observations supported right now are 'status' and 'progress'. The 'status' observations are generated automatically when you use _add_status() or _add_status_head() (see above).

Generally 'progress' notifications are accompanied by a simple text message. You may also pass as a third argument a hashref. This hashref gives us room to grow and the observers the ability to differentiate among progress messages. For now, the hashref only supports one key: long. If you're posting a progress notification of a process that will take a long time, set this to 'yes' so the observer can differentiate -- let the user know it will take a while, etc.

 sub run_task {
     my ( $self ) = @_;
     $self->_do_some_simple( 'thing' );
     $self->notify_observers( progress => 'Simple thing complete' );
     $self->_do_some_other( @stuff );
     $self->notify_observers( progress => 'Other stuff complete' );
     $self->notify_observers( progress => 'Preparing complex task',
                              { long => 'yes' } );
     $self->_do_complex_task;
     $self->notify_observers( progress => 'Complex task complete' );
     # This fires an implicit observation of type 'status'
     $self->_add_status( { is_ok   => 'yes',
                           message => 'Foobar task ok' } );
 }

This is a contrived example -- if your task is very simple (like this) you probably don't need to bother with observations. The notifications generated by the status messages will be more than adequate.

However, if you're looping through a set of packages, or performing a complicated set of operations, it can be very helpful for your users to let them know things are actually happening.

Example

Here is an example of a direct subclass that just creates a file 'hello_world' in the website directory:

 package My::Task;
 
 use strict;
 use base qw( OpenInteract2::Manage );
 
 sub param_required { return [ 'website_dir' ] }
 
 sub run_task {
     my ( $self ) = @_;
     my $website_dir = $self->param( 'website_dir' );
     $website_dir =~ s|/$||;
     my $filename = File::Spec->catfile( $website_dir, 'hello_world' );
     my %status = ();
     if ( -f $filename ) {
         $status{message} = "Could not create [$filename]: already exists";
         $status{is_ok}   = 'no';
         $self->_add_status( \%status );
         return;
     }
     eval { open( HW, "> $filename" ) || die $! };
     if ( $@ ) {
         $status{message} = "Cannot write to [$filename]: $@";
         $status{is_ok}   = 'no';
     }
     else {
         print HW "Hello from My::Task!";
         close( HW );
         $status{is_ok}   = 'yes';
         $status{message} = "File [$filename] created ok";
     }
     $self->_add_status( \%status );
 }
 
 1;

And here is how you would register and run your task:

 #!/usr/bin/perl
 
 use strict;
 use OpenInteract2::Manage;
 
 OpenInteract2::Manage->register_task( hello_world => 'My::Task' );
 
 my $task = OpenInteract2::Manage->new( 'hello_world',
                                       { website_dir => $ENV{OPENINTERACT2} } );
 my @status = eval { $task->execute };
 if ( $@ ) {
     print "Task failed to run: $@";
 }
 else {
     foreach my $s ( @status ) {
         print "Task OK? $s->{is_ok}\n",
               "$s->{message}\n";
     }
 }

BUGS

None yet.

TO DO

Get everything working...

SEE ALSO

Class::Factory

OpenInteract2::Manage::Package

OpenInteract2::Manage::Website

OpenInteract2::Setup

COPYRIGHT

Copyright (c) 2002-2003 Chris Winters. All rights reserved.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

AUTHORS

Chris Winters <chris@cwinters.com>