HiveGeneric_conf.pm 22.2 KB
Newer Older
Leo Gordon's avatar
Leo Gordon committed
1 2 3 4 5

=pod 

=head1 NAME

Leo Gordon's avatar
Leo Gordon committed
6
Bio::EnsEMBL::Hive::PipeConfig::HiveGeneric_conf
Leo Gordon's avatar
Leo Gordon committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

=head1 SYNOPSIS

    # Example 1: specifying only the mandatory option:
init_pipeline.pl Bio::EnsEMBL::Hive::PipeConfig::HiveGeneric_conf -password <mypass>

    # Example 2: specifying the mandatory options as well as overriding some defaults:
init_pipeline.pl Bio::EnsEMBL::Hive::PipeConfig::HiveGeneric_conf -ensembl_cvs_root_dir ~/ensembl_main -pipeline_db -host <myhost> -pipeline_db -dbname <mydbname> -password <mypass>

=head1 DESCRIPTION

Generic configuration module for all Hive pipelines with loader functionality.
All other Hive PipeConfig modules should inherit from this module and will probably need to redefine some or all of the following interface methods:

    * default_options:              returns a hash of (possibly multilevel) defaults for the options on which depend the rest of the configuration

    * pipeline_create_commands:     returns a list of strings that will be executed as system commands needed to create and set up the pipeline database

    * pipeline_wide_parameters:     returns a hash of pipeline-wide parameter names and their values

    * resource_classes:             returns a hash of resource class definitions

    * pipeline_analyses:            returns a list of hash structures that define analysis objects bundled with definitions of corresponding jobs, rules and resources

When defining anything except the keys of default_options() a call to $self->o('myoption') can be used.
This call means "substitute this call for the value of 'myoption' at the time of configuring the pipeline".
All option names mentioned in $self->o() calls within the five interface methods above can be given non-default values from the command line.

Please make sure you have studied the pipeline configuraton examples in Bio::EnsEMBL::Hive::PipeConfig before creating your own PipeConfig modules.

=head1 CONTACT

  Please contact ehive-users@ebi.ac.uk mailing list with questions/suggestions.

=cut

43 44 45 46 47 48 49 50 51

package Bio::EnsEMBL::Hive::PipeConfig::HiveGeneric_conf;

use strict;
use warnings;
use Getopt::Long;
use Bio::EnsEMBL::Utils::Argument;          # import 'rearrange()'
use Bio::EnsEMBL::Hive::Utils 'stringify';  # import 'stringify()'
use Bio::EnsEMBL::Hive::DBSQL::DBAdaptor;
52
use Bio::EnsEMBL::Hive::DBSQL::AnalysisJobAdaptor;
53 54 55 56
use Bio::EnsEMBL::Hive::Extensions;

# ---------------------------[the following methods will be overridden by specific pipelines]-------------------------

Leo Gordon's avatar
Leo Gordon committed
57 58 59 60 61 62 63
=head2 default_options

    Description : Interface method that should return a hash of option_name->default_option_value pairs.
                  Please see existing PipeConfig modules for examples.

=cut

64 65 66 67 68
sub default_options {
    my ($self) = @_;
    return {
        'ensembl_cvs_root_dir' => $ENV{'HOME'}.'/work',     # some Compara developers might prefer $ENV{'HOME'}.'/ensembl_main'

Leo Gordon's avatar
Leo Gordon committed
69
        'pipeline_name' => 'hive_generic',
70 71 72 73 74 75 76 77 78 79 80

        'pipeline_db'   => {
            -host   => 'compara3',
            -port   => 3306,
            -user   => 'ensadmin',
            -pass   => $self->o('password'),
            -dbname => $ENV{'USER'}.'_'.$self->o('pipeline_name'),  # example of a linked definition (resolved via saturation)
        },
    };
}

Leo Gordon's avatar
Leo Gordon committed
81 82 83 84 85 86 87
=head2 pipeline_create_commands

    Description : Interface method that should return a list of command lines to be run in order to create and set up the pipeline database.
                  Please see existing PipeConfig modules for examples.

=cut

88 89 90 91 92 93 94 95 96 97 98
sub pipeline_create_commands {
    my ($self) = @_;
    return [
        'mysql '.$self->dbconn_2_mysql('pipeline_db', 0)." -e 'CREATE DATABASE ".$self->o('pipeline_db', '-dbname')."'",

            # standard eHive tables and procedures:
        'mysql '.$self->dbconn_2_mysql('pipeline_db', 1).' <'.$self->o('ensembl_cvs_root_dir').'/ensembl-hive/sql/tables.sql',
        'mysql '.$self->dbconn_2_mysql('pipeline_db', 1).' <'.$self->o('ensembl_cvs_root_dir').'/ensembl-hive/sql/procedures.sql',
    ];
}

Leo Gordon's avatar
Leo Gordon committed
99 100 101 102 103 104 105 106
=head2 pipeline_wide_parameters

    Description : Interface method that should return a hash of pipeline_wide_parameter_name->pipeline_wide_parameter_value pairs.
                  The value doesn't have to be a scalar, can be any Perl structure now (will be stringified and de-stringified automagically).
                  Please see existing PipeConfig modules for examples.

=cut

107 108 109 110 111 112 113
sub pipeline_wide_parameters {
    my ($self) = @_;
    return {
        'pipeline_name'  => $self->o('pipeline_name'),       # name the pipeline to differentiate the submitted processes
    };
}

Leo Gordon's avatar
Leo Gordon committed
114 115 116 117 118 119 120
=head2 resource_classes

    Description : Interface method that should return a hash of resource_description_id->resource_description_hash.
                  Please see existing PipeConfig modules for examples.

=cut

121 122 123 124 125 126 127 128
sub resource_classes {
    my ($self) = @_;
    return {
        0 => { -desc => 'default, 8h',      'LSF' => '' },
        1 => { -desc => 'urgent',           'LSF' => '-q yesterday' },
    };
}

Leo Gordon's avatar
Leo Gordon committed
129 130 131 132 133 134 135
=head2 pipeline_analyses

    Description : Interface method that should return a list of hashes that define analysis bundled with corresponding jobs, dataflow and analysis_ctrl rules and resource_id.
                  Please see existing PipeConfig modules for examples.

=cut

136 137 138 139 140 141 142 143 144 145 146
sub pipeline_analyses {
    my ($self) = @_;
    return [
    ];
}


# ---------------------------------[now comes the interfacing stuff - feel free to call but not to modify]--------------------

my $undef_const = '-=[UnDeFiNeD_VaLuE]=-';  # we don't use undef, as it cannot be detected as a part of a string

Leo Gordon's avatar
Leo Gordon committed
147 148 149 150 151 152 153
=head2 new

    Description : Just a trivial constructor for this type of objects.
    Caller      : init_pipeline.pl or any other script that will drive this module.

=cut

154 155 156 157 158 159 160 161
sub new {
    my ($class) = @_;

    my $self = bless {}, $class;

    return $self;
}

Leo Gordon's avatar
Leo Gordon committed
162 163 164
=head2 o

    Description : This is the method you call in the interface methods when you need to substitute an option: $self->o('password') .
165
                  To reach down several levels of a multilevel option (such as $self->('pipeline_db') ) just list the keys down the desired path: $self->o('pipeline_db', '-user') .
Leo Gordon's avatar
Leo Gordon committed
166 167 168

=cut

169 170 171 172 173 174
sub o {                 # descends the option hash structure (vivifying all encountered nodes) and returns the value if found
    my $self = shift @_;

    my $value = $self->{_pipe_option} ||= {};

    while(defined(my $option_syll = shift @_)) {
Leo Gordon's avatar
bug fix  
Leo Gordon committed
175 176

        if(exists($value->{$option_syll})
177
        and ((ref($value->{$option_syll}) eq 'HASH') or _completely_defined_string($value->{$option_syll}))
Leo Gordon's avatar
bug fix  
Leo Gordon committed
178
        ) {
179 180 181 182 183 184 185 186 187 188
            $value = $value->{$option_syll};            # just descend one level
        } elsif(@_) {
            $value = $value->{$option_syll} = {};       # force intermediate level vivification
        } else {
            $value = $value->{$option_syll} = $undef_const;    # force leaf level vivification
        }
    }
    return $value;
}

Leo Gordon's avatar
Leo Gordon committed
189 190 191 192 193 194
=head2 dbconn_2_mysql

    Description : A convenience method used to stringify a connection-parameters hash into a parameter string that both mysql and beekeeper.pl can understand

=cut

195 196 197 198 199
sub dbconn_2_mysql {    # will save you a lot of typing
    my ($self, $db_conn, $with_db) = @_;

    return '--host='.$self->o($db_conn,'-host').' '
          .'--port='.$self->o($db_conn,'-port').' '
200 201
          .'--user="'.$self->o($db_conn,'-user').'" '
          .'--pass="'.$self->o($db_conn,'-pass').'" '
202
          .($with_db ? ($self->o($db_conn,'-dbname').' ') : '');
203 204
}

Leo Gordon's avatar
Leo Gordon committed
205 206 207 208 209 210
=head2 dbconn_2_url

    Description :  A convenience method used to stringify a connection-parameters hash into a 'url' that beekeeper.pl will undestand

=cut

211 212 213 214 215 216
sub dbconn_2_url {
    my ($self, $db_conn) = @_;

    return 'mysql://'.$self->o($db_conn,'-user').':'.$self->o($db_conn,'-pass').'@'.$self->o($db_conn,'-host').':'.$self->o($db_conn,'-port').'/'.$self->o($db_conn,'-dbname');
}

Leo Gordon's avatar
Leo Gordon committed
217 218 219 220 221 222 223 224 225 226 227 228 229
=head2 process_options

    Description : The method that does all the parameter parsing magic.
                  It is two-pass through the interface methods: first pass collects the options, second is intelligent substitution.

    Caller      : init_pipeline.pl or any other script that will drive this module.

    Note        : You can override parsing the command line bit by providing a hash as the argument to this method.
                  This hash should contain definitions of all the parameters you would otherwise be providing from the command line.
                  Useful if you are creating batches of hive pipelines using a script.

=cut

230 231 232 233 234 235 236
sub process_options {
    my $self            = shift @_;

        # first, vivify all options in $self->o()
    $self->default_options();
    $self->pipeline_create_commands();
    $self->pipeline_wide_parameters();
Leo Gordon's avatar
Leo Gordon committed
237
    $self->resource_classes();
238
    $self->pipeline_analyses();
239
    $self->dbconn_2_url('pipeline_db'); # force vivification of the whole 'pipeline_db' structure (used in run() )
240 241

        # you can override parsing of commandline options if creating pipelines by a script - just provide the overriding hash
Leo Gordon's avatar
Leo Gordon committed
242
    my $cmdline_options = $self->{_cmdline_options} = shift @_ || $self->_load_cmdline_options();
243

Leo Gordon's avatar
Leo Gordon committed
244
    print "\nPipeline:\n\t".ref($self)."\n\n";
245 246 247

    if($cmdline_options->{'help'}) {

Leo Gordon's avatar
Leo Gordon committed
248
        my $all_needed_options = $self->_hash_undefs();
249

Leo Gordon's avatar
Leo Gordon committed
250
        $self->_saturated_merge_defaults_into_options();
251

Leo Gordon's avatar
Leo Gordon committed
252
        my $mandatory_options = $self->_hash_undefs();
253

254 255 256 257 258
        print "Mandatory options:\n";
        foreach my $key (sort keys %$mandatory_options) {
            print "\t$key\n";
        }
        print "Pre-defined options:\n";
259
        foreach my $key (sort keys %$all_needed_options) {
260 261 262
            unless($mandatory_options->{$key}) {
                print "\t$key\n";
            }
263
        }
264

265 266 267 268
        exit(0);

    } else {

Leo Gordon's avatar
Leo Gordon committed
269
        $self->_merge_into_options($cmdline_options);
270

Leo Gordon's avatar
Leo Gordon committed
271
        $self->_saturated_merge_defaults_into_options();
272

Leo Gordon's avatar
Leo Gordon committed
273
        my $undefined_options = $self->_hash_undefs();
274 275 276 277 278 279 280 281 282 283 284 285

        if(scalar(keys(%$undefined_options))) {
            print "Undefined options:\n\n";
            print join("\n", map { "\t$_" } keys %$undefined_options)."\n\n";
            print "To get the list of available options for ".ref($self)." pipeline please run:\n\n";
            print "\t$0 ".ref($self)." -help\n\n";
            exit(1);
        }
    }
    # by this point we have either exited or options are good
}

Leo Gordon's avatar
Leo Gordon committed
286 287 288 289 290 291 292 293
=head2 run

    Description : The method that uses the Hive/EnsEMBL API to actually create all the analyses, jobs, dataflow and control rules and resource descriptions.

    Caller      : init_pipeline.pl or any other script that will drive this module.

=cut

294
sub run {
295 296 297
    my $self  = shift @_;
    my $analysis_topup = $self->{_cmdline_options}{'analysis_topup'};
    my $job_topup      = $self->{_cmdline_options}{'job_topup'};
298

299
    unless($analysis_topup || $job_topup) {
300 301 302 303 304 305 306 307 308 309 310 311
        foreach my $cmd (@{$self->pipeline_create_commands}) {
            warn "Running the command:\n\t$cmd\n";
            if(my $retval = system($cmd)) {
                die "Return value = $retval, possibly an error\n";
            } else {
                warn "Done.\n\n";
            }
        }
    }

    my $hive_dba                     = new Bio::EnsEMBL::Hive::DBSQL::DBAdaptor(%{$self->o('pipeline_db')});
    
312 313 314 315 316 317 318 319 320 321
    unless($job_topup) {
        my $meta_container = $hive_dba->get_MetaContainer;
        warn "Loading pipeline-wide parameters ...\n";

        my $pipeline_wide_parameters = $self->pipeline_wide_parameters;
        while( my($meta_key, $meta_value) = each %$pipeline_wide_parameters ) {
            if($analysis_topup) {
                $meta_container->delete_key($meta_key);
            }
            $meta_container->store_key_value($meta_key, stringify($meta_value));
322
        }
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
        warn "Done.\n\n";

            # pre-load the resource_description table
        my $resource_description_adaptor = $hive_dba->get_ResourceDescriptionAdaptor;
        warn "Loading the ResourceDescriptions ...\n";

        my $resource_classes = $self->resource_classes;
        while( my($rc_id, $mt2param) = each %$resource_classes ) {
            my $description = delete $mt2param->{-desc};
            while( my($meadow_type, $xparams) = each %$mt2param ) {
                $resource_description_adaptor->create_new(
                    -RC_ID       => $rc_id,
                    -MEADOW_TYPE => $meadow_type,
                    -PARAMETERS  => $xparams,
                    -DESCRIPTION => $description,
                );
            }
340
        }
341
        warn "Done.\n\n";
342 343 344 345 346
    }

    my $analysis_adaptor             = $hive_dba->get_AnalysisAdaptor;

    foreach my $aha (@{$self->pipeline_analyses}) {
347 348
        my ($logic_name, $module, $parameters_hash, $input_ids, $program_file, $blocked, $batch_size, $hive_capacity, $failed_job_tolerance, $can_be_empty, $rc_id) =
             rearrange([qw(logic_name module parameters input_ids program_file blocked batch_size hive_capacity failed_job_tolerance can_be_empty rc_id)], %$aha);
349

Leo Gordon's avatar
Leo Gordon committed
350 351 352
        $parameters_hash ||= {};
        $input_ids       ||= [];

353
        if($analysis_topup and $analysis_adaptor->fetch_by_logic_name($logic_name)) {
354 355 356 357
            warn "Skipping already existing analysis '$logic_name'\n";
            next;
        }

358
        my $analysis;
359

360
        if($job_topup) {
361

362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
            $analysis = $analysis_adaptor->fetch_by_logic_name($logic_name) || die "Could not fetch analysis '$logic_name'";

        } else {

            warn "Creating '$logic_name'...\n";

            $analysis = Bio::EnsEMBL::Analysis->new (
                -db              => '',
                -db_file         => '',
                -db_version      => '1',
                -logic_name      => $logic_name,
                -module          => $module,
                -parameters      => stringify($parameters_hash),    # have to stringify it here, because Analysis code is external wrt Hive code
                -program_file    => $program_file,
            );
377

378 379 380 381 382 383 384
            $analysis_adaptor->store($analysis);

            my $stats = $analysis->stats();
            $stats->batch_size( $batch_size )                       if(defined($batch_size));
            $stats->hive_capacity( $hive_capacity )                 if(defined($hive_capacity));
            $stats->failed_job_tolerance( $failed_job_tolerance )   if(defined($failed_job_tolerance));
            $stats->rc_id( $rc_id )                                 if(defined($rc_id));
385
            $stats->can_be_empty( $can_be_empty )                   if(defined($can_be_empty));
386 387 388
            $stats->status($blocked ? 'BLOCKED' : 'READY');         #   (some analyses will be waiting for human intervention in blocked state)
            $stats->update();
        }
389 390 391 392 393 394 395

            # now create the corresponding jobs (if there are any):
        foreach my $input_id_hash (@$input_ids) {

            Bio::EnsEMBL::Hive::DBSQL::AnalysisJobAdaptor->CreateNewJob(
                -input_id       => $input_id_hash,  # input_ids are now centrally stringified in the AnalysisJobAdaptor
                -analysis       => $analysis,
396
                -input_job_id   => undef, # these jobs are created by the initialization script, not by another job
397 398 399 400
            );
        }
    }

401
    unless($job_topup) {
402

403 404 405 406
            # Now, run separately through the already created analyses and link them together:
            #
        my $ctrl_rule_adaptor            = $hive_dba->get_AnalysisCtrlRuleAdaptor;
        my $dataflow_rule_adaptor        = $hive_dba->get_DataflowRuleAdaptor;
407

408 409 410
        foreach my $aha (@{$self->pipeline_analyses}) {
            my ($logic_name, $wait_for, $flow_into) =
                 rearrange([qw(logic_name wait_for flow_into)], %$aha);
411

412
            my $analysis = $analysis_adaptor->fetch_by_logic_name($logic_name);
413

414 415 416 417
            $wait_for ||= [];
            $wait_for   = [ $wait_for ] unless(ref($wait_for) eq 'ARRAY'); # force scalar into an arrayref

                # create control rules:
418 419
            foreach my $condition_url (@$wait_for) {
                if(my $condition_analysis = $analysis_adaptor->fetch_by_logic_name_or_url($condition_url)) {
420 421 422 423 424

                    my $new_cfr    = $ctrl_rule_adaptor->create_rule( $condition_analysis, $analysis);
                    my $cfr_action = $new_cfr ? 'Created a new' : 'Found an existing';

                    warn "$cfr_action Control rule: $condition_url -| $logic_name\n";
425
                } else {
426
                    die "Could not fetch analysis '$condition_url' to create a control rule";
427
                }
428 429
            }

430 431
            $flow_into ||= {};
            $flow_into   = { 1 => $flow_into } unless(ref($flow_into) eq 'HASH'); # force non-hash into a hash
432

433 434
            foreach my $branch_code (sort {$a <=> $b} keys %$flow_into) {
                my $heirs = $flow_into->{$branch_code};
435

436
                $heirs = [ $heirs ] unless(ref($heirs)); # force scalar into an arrayref first
437

438
                $heirs = { map { ($_ => undef) } @$heirs } if(ref($heirs) eq 'ARRAY'); # now force it into a hash if it wasn't
439

440 441 442 443
                while(my ($heir_url, $input_id_template) = each %$heirs) {

                    my $heir_analysis = $analysis_adaptor->fetch_by_logic_name_or_url($heir_url);

444 445
                    my $new_dfr    = $dataflow_rule_adaptor->create_rule( $analysis, $heir_analysis || $heir_url, $branch_code, $input_id_template);
                    my $dfr_action = $new_dfr ? 'Created a new' : 'Found an existing';
446

447
                    warn "$dfr_action DataFlow rule: [$branch_code] $logic_name -> $heir_url"
448
                        .($input_id_template ? ' WITH TEMPLATE: '.stringify($input_id_template) : '')."\n";
449 450 451 452 453 454 455 456
                }
            }
        }
    }

    my $url = $self->dbconn_2_url('pipeline_db');

    print "\n\n\tPlease run the following commands:\n\n";
Leo Gordon's avatar
Leo Gordon committed
457 458 459 460
    print "  beekeeper.pl -url $url -sync\t\t# (synchronize the Hive - should always be done before [re]starting a pipeline)\n\n";
    print "  beekeeper.pl -url $url -loop\t\t# (run the pipeline in automatic mode)\n";
    print "(OR)\n";
    print "  beekeeper.pl -url $url -run\t\t# (run one step of the pipeline - useful for debugging/learning)\n";
Leo Gordon's avatar
Leo Gordon committed
461 462 463

    print "\n\n\tTo connect to your pipeline database use the following line:\n\n";
    print "  mysql ".$self->dbconn_2_mysql('pipeline_db',1)."\n\n";
464 465 466 467 468
}


# -------------------------------[the rest are dirty implementation details]-------------------------------------

469 470

=head2 _completely_defined_string
Leo Gordon's avatar
Leo Gordon committed
471 472 473 474 475

    Description : a private function (not a method) that checks whether a certain string is clean from undefined options

=cut

476
sub _completely_defined_string {
477 478 479
    return (index(shift @_, $undef_const) == ($[-1) );  # i.e. $undef_const is not a substring
}

480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505

=head2 _completely_defined_structure

    Description : a private function (not a method) that checks whether a certain structure is clean from undefined options

=cut

sub _completely_defined_structure {
    my $structure = shift @_;

    if(ref($structure) eq 'HASH') {
        while(my ($key, $value) = each %$structure) {
            return 0 unless(_completely_defined_structure($value));
        }
        return 1;
    } elsif(ref($structure) eq 'ARRAY') {
        foreach my $element (@$structure) {
            return 0 unless(_completely_defined_structure($element));
        }
        return 1;
    } else {
        return _completely_defined_string($structure);
    }
}


Leo Gordon's avatar
Leo Gordon committed
506 507 508 509 510 511 512
=head2 _load_cmdline_options

    Description : a private method that deals with parsing of the command line (currently it drives GetOptions that has some limitations)

=cut

sub _load_cmdline_options {
513 514 515 516 517 518
    my $self      = shift @_;

    my %cmdline_options = ();

    GetOptions( \%cmdline_options,
        'help!',
519 520
        'analysis_topup!',
        'job_topup!',
521 522 523 524 525
        map { "$_=s".((ref($self->o($_)) eq 'HASH') ? '%' : '') } keys %{$self->o}
    );
    return \%cmdline_options;
}

526

Leo Gordon's avatar
Leo Gordon committed
527 528 529 530 531 532 533
=head2 _merge_into_options

    Description : a private method to merge one options-containing structure into another

=cut

sub _merge_into_options {
534 535 536 537 538 539
    my $self      = shift @_;
    my $hash_from = shift @_;
    my $hash_to   = shift @_ || $self->o;

    my $subst_counter = 0;

540 541 542 543 544
    while(my($key, $from_value) = each %$hash_from) {
        if( exists($hash_to->{$key})        # i.e. if there is interest. Only pay attention at options that are actually used in the PipeConfig
        and !_completely_defined_structure($hash_to->{$key})
        ) {
            if(ref($from_value) eq 'HASH') {
545
                if(ref($hash_to->{$key}) eq 'HASH') {
546 547
                    my $rec_subst   = $self->_merge_into_options($from_value, $hash_to->{$key});
                    $subst_counter += $rec_subst;
548
                } else {
549 550
                    $hash_to->{$key} = { %$from_value };
                    $subst_counter += scalar(keys %$from_value);
551
                }
552 553
            } elsif(_completely_defined_structure($from_value)) {
                $hash_to->{$key} = $from_value;
554 555 556 557 558 559 560
                $subst_counter++;
            }
        }
    }
    return $subst_counter;
}

Leo Gordon's avatar
Leo Gordon committed
561 562 563 564 565 566 567 568
=head2 _saturated_merge_defaults_into_options

    Description : a private method to merge defaults into options as many times as required to resolve the dependencies.
                  Use with caution, as it doesn't check for loops!

=cut

sub _saturated_merge_defaults_into_options {
569 570 571
    my $self      = shift @_;

        # Note: every time the $self->default_options() has to be called afresh, do not cache!
Leo Gordon's avatar
Leo Gordon committed
572
    while(my $res = $self->_merge_into_options($self->default_options)) { }
573 574
}

Leo Gordon's avatar
Leo Gordon committed
575 576 577 578 579 580 581 582
=head2 _hash_undefs

    Description : a private method that collects all the options that are undefined at the moment
                  (used at different stages to find 'all_options', 'mandatory_options' and 'undefined_options').

=cut

sub _hash_undefs {
583 584
    my $self      = shift @_;
    my $hash_to   = shift @_ || {};
585
    my $source    = shift @_; unless(defined($source)) { $source = $self->o; }
586 587
    my $prefix    = shift @_ || '';

588 589 590 591 592 593 594 595 596 597
    if(ref($source) eq 'HASH') {
        while(my ($key, $value) = each %$source) {
            my $hash_element_prefix = ($prefix ? "$prefix->" : '') . "{'$key'}";

            $self->_hash_undefs($hash_to, $value, $hash_element_prefix);
        }
    } elsif(ref($source) eq 'ARRAY') {
        foreach my $index (0..scalar(@$source)-1) {
            my $element = $source->[$index];
            my $array_element_prefix = ($prefix ? "$prefix->" : '') . "[$index]";
598

599
            $self->_hash_undefs($hash_to, $element, $array_element_prefix);
600
        }
601
    } elsif(!_completely_defined_string($source)) {
602
        $hash_to->{$prefix} = 1;
603
    }
604

605 606 607 608
    return $hash_to;
}

1;