File Coverage

blib/lib/SPOPS/ClassFactory.pm
Criterion Covered Total %
statement 48 241 19.9
branch 0 94 0.0
condition 0 28 0.0
subroutine 16 32 50.0
pod 0 14 0.0
total 64 409 15.6


line stmt bran cond sub pod time code
1             package SPOPS::ClassFactory;
2              
3             # $Id: ClassFactory.pm,v 3.6 2004/06/02 00:48:21 lachoy Exp $
4              
5 17     17   111 use strict;
  17         37  
  17         796  
6 17     17   96 use base qw( Exporter );
  17         37  
  17         1727  
7 17     17   103 use Log::Log4perl qw( get_logger );
  17         33  
  17         106  
8              
9 17     17   25712 use Class::ISA;
  17         69205  
  17         579  
10 17     17   158 use Data::Dumper qw( Dumper );
  17         403  
  17         1270  
11 17     17   269 use SPOPS;
  17         51  
  17         762  
12 17     17   10775 use SPOPS::Exception;
  17         53  
  17         189  
13              
14             $SPOPS::ClassFactory::VERSION = sprintf("%d.%02d", q$Revision: 3.6 $ =~ /(\d+)\.(\d+)/);
15             @SPOPS::ClassFactory::EXPORT_OK = qw( OK DONE NOTIFY ERROR RESTART
16             FACTORY_METHOD RULESET_METHOD );
17              
18 17     17   1756 use constant OK => 'OK';
  17         33  
  17         1137  
19 17     17   163 use constant DONE => 'DONE';
  17         200  
  17         850  
20 17     17   98 use constant NOTIFY => 'NOTIFY';
  17         33  
  17         5882  
21 17     17   105 use constant ERROR => 'ERROR';
  17         42  
  17         1827  
22 17     17   96 use constant RESTART => 'RESTART';
  17         36  
  17         866  
23 17     17   85 use constant FACTORY_METHOD => 'behavior_factory';
  17         36  
  17         957  
24 17     17   84 use constant RULESET_METHOD => 'ruleset_factory';
  17         110  
  17         51848  
25              
26             my $log = get_logger();
27              
28             my %REQ_CLASSES = ();
29              
30             my $PK = '__private__'; # Save typing...
31              
32             # TODO: Export constants with the names of these slots -- the order
33             # doesn't matter to anyone except us, so we shouldn't need to export
34             # order and be able to keep the variable a lexical
35              
36             my @SLOTS = qw(
37             manipulate_configuration id_method read_code
38             fetch_by has_a links_to add_rule
39             );
40              
41             my %SLOT_NUM = map { $SLOTS[ $_ ] => $_ } ( 0 .. ( scalar @SLOTS - 1 ) );
42              
43              
44             ########################################
45             # MAIN INTERFACE
46             ########################################
47              
48             # TODO: Will $config ever be an object? Also, is 'create' the best
49             # name?
50              
51             sub create {
52 0     0 0   my ( $class, $all_config, $p ) = @_;
53 0 0         return [] unless ( ref $all_config eq 'HASH' );
54 0   0       $p ||= {};
55              
56 0           $class->create_all_stubs( $all_config, $p );
57 0           $class->find_all_behavior( $all_config, $p );
58 0           $class->exec_all_behavior( $all_config, $p );
59 0           $class->clean_all_behavior( $all_config, $p );
60              
61 0           my $alias_list = $class->get_alias_list( $all_config, $p );
62 0           return [ map { $all_config->{ $_ }->{class} }
  0            
63 0           grep { defined $all_config->{ $_ }->{class} }
64 0           @{ $alias_list } ];
65             }
66              
67              
68             ########################################
69             # MULTI-CONFIG METHODS
70             ########################################
71              
72             # These methods operate on $all_config, a hashref of SPOPS
73             # configuration hashrefs
74              
75              
76             # First, we need to create the class so we can have an inheritance
77             # tree to walk -- think of this as the ur-behavior, or the beginning
78             # of the chicken-and-egg, or...
79              
80             sub create_all_stubs {
81 0     0 0   my ( $class, $all_config, $p ) = @_;
82 0           my $alias_list = $class->get_alias_list( $all_config, $p );
83 0           foreach my $alias ( @{ $alias_list } ) {
  0            
84 0           my $this_class = $all_config->{ $alias }{class};
85 0   0       $all_config->{ $alias }->{main_alias} ||= $alias;
86 0           my ( $status, $msg ) = $class->create_stub( $all_config->{ $alias } );
87 0 0         if ( $status eq ERROR ) { SPOPS::Exception->throw( $msg ) }
  0            
88 0           my ( $cfg_status, $cfg_msg ) = $class->install_configuration( $this_class, $all_config->{ $alias } );
89 0 0         if ( $cfg_status eq ERROR ) { SPOPS::Exception->throw( $cfg_msg ) }
  0            
90             }
91             }
92              
93              
94             # Now that the class is created with at least @ISA defined, we can
95             # walk through @ISA for each class and install all the behaviors
96              
97             sub find_all_behavior {
98 0     0 0   my ( $class, $all_config, $p ) = @_;
99 0           my $alias_list = $class->get_alias_list( $all_config, $p );
100 0           foreach my $alias ( @{ $alias_list } ) {
  0            
101 0           my $this_class = $all_config->{ $alias }{class};
102 0           my $this_config = $this_class->CONFIG;
103 0           $this_config->{ $PK }{behavior_table} = {};
104 0           $this_config->{ $PK }{behavior_run} = {};
105 0           $this_config->{ $PK }{behavior_map} = $class->find_behavior( $this_class );
106             }
107             }
108              
109              
110             # Now execute the behavior for each slot-and-alias. Note that we
111             # cannot do this in reverse order (alias-and-slot) because some later
112             # slots (particularly the relationship ones) may depend on earlier
113             # slots being executed for other classes.
114              
115             sub exec_all_behavior {
116 0     0 0   my ( $class, $all_config, $p ) = @_;
117 0           my $alias_list = $class->get_alias_list( $all_config, $p );
118 0           foreach my $slot_name ( @SLOTS ) {
119 0           foreach my $alias ( @{ $alias_list } ) {
  0            
120 0           my $this_class = $all_config->{ $alias }{class};
121 0           $class->exec_behavior( $slot_name, $this_class );
122             }
123             }
124             }
125              
126              
127             # Remove all evidence of behaviors, tracking, etc. -- nobody should
128             # need this information once the class has been created.
129              
130             sub clean_all_behavior {
131 0     0 0   my ( $class, $all_config, $p ) = @_;
132 0           my $alias_list = $class->get_alias_list( $all_config, $p );
133 0           foreach my $alias ( @{ $alias_list } ) {
  0            
134 0           my $this_class = $all_config->{ $alias }{class};
135 0           delete $this_class->CONFIG->{ $PK };
136             }
137             }
138              
139              
140             ########################################
141             # CREATE CLASS
142             ########################################
143              
144             # EVAL'ABLE PACKAGE/SUBROUTINES
145              
146             # Here's our template for a module on the fly. Super easy.
147              
148             my $GENERIC_TEMPLATE = <<'PACKAGE';
149             @%%CLASS%%::ISA = qw( %%ISA%% );
150             $%%CLASS%%::C = {};
151             sub %%CLASS%%::CONFIG { return $%%CLASS%%::C; }
152             PACKAGE
153              
154             sub create_stub {
155 0     0 0   my ( $class, $config ) = @_;
156              
157 0           my $this_class = $config->{class};
158 0 0         $log->is_debug &&
159             $log->debug( "Creating stub ($this_class) with main alias ($config->{main_alias})");
160              
161             # Create the barest information forming the class; just substitute our
162             # keywords (currently only the class name) for the items in the
163             # generic template above.
164              
165 0           my $module = $GENERIC_TEMPLATE;
166 0           $module =~ s/%%CLASS%%/$this_class/g;
167 0           my $isa_listing = join( ' ', @{ $config->{isa} } );
  0            
168 0           $module =~ s/%%ISA%%/$isa_listing/g;
169              
170 0 0         $log->is_debug &&
171             $log->debug( "Trying to create class with the code:\n$module\n" );
172              
173             # Capture 'warn' calls that get triggered as a result of warnings,
174             # redefined subroutines or whatnot; these get dumped to STDERR and
175             # we want to be as quiet as possible -- or at least control our
176             # noise!
177              
178             {
179 0     0     local $SIG{__WARN__} = sub { return undef };
  0            
  0            
180 0           eval $module;
181 0 0         if ( $@ ) {
182 0           return ( ERROR, "Error creating stub class [$this_class] with " .
183             "code\n$module\nError: $@" );
184             }
185             }
186 0           return $class->require_config_classes( $config );
187             }
188              
189              
190             # Just step through @{ $config->{isa} } and 'require' each entry
191             # unless it's already been; also require everything in
192             # @{ $config->{rules_from} }
193              
194             sub require_config_classes {
195 0     0 0   my ( $class, $config ) = @_;
196 0           my $this_class = $config->{class};
197 0   0       my $rules_from = $config->{rules_from} || [];
198 0           foreach my $req_class ( @{ $config->{isa} }, @{ $rules_from } ) {
  0            
  0            
199 0 0         next unless ( $req_class );
200 0 0         next if ( $REQ_CLASSES{ $req_class } );
201 0           eval "require $req_class";
202 0 0         if ( $@ ) {
203 0           return ( ERROR, "Error requiring class [$req_class] from ISA " .
204             "or 'rules_from' in [$this_class]: $@" );
205             }
206             $log->is_debug &&
207 0 0         $log->debug( "Class [$req_class] require'd by [$this_class] ok." );
208 0           $REQ_CLASSES{ $req_class }++;
209             }
210 0           return ( OK, undef );
211             }
212              
213              
214             ########################################
215             # INSTALL CONFIGURATION
216             ########################################
217              
218             # Just take the config from the $all_config (or wherever) and install
219             # it to the class -- we aren't doing any manipulation or anything,
220             # just copying over the original config key by key. Manipulation comes later.
221              
222             sub install_configuration {
223 0     0 0   my ( $class, $this_class, $config ) = @_;
224 0 0         $log->is_info &&
225             $log->info( "Installing configuration to class ($this_class)" );
226 0 0         $log->is_debug &&
227             $log->debug( "Config ($this_class)\n", Dumper( $config ) );
228 0           my $class_config = $this_class->CONFIG;
229 0           while ( my ( $k, $v ) = each %{ $config } ) {
  0            
230 0           $class_config->{ $k } = $v;
231             }
232 0           return ( OK, undef );
233             }
234              
235              
236             ########################################
237             # FIND BEHAVIOR
238             ########################################
239              
240             # Find all the factory method-generators in all members of a class's
241             # ISA, then run each of the generators and keep track of the slots
242             # each generator uses (behavior map)
243              
244             sub find_behavior {
245 0     0 0   my ( $class, $this_class ) = @_;
246 0           my $this_config = $this_class->CONFIG;
247              
248             # Allow config to specify ClassFactory-only classes in
249             # 'rules_from' configuration key.
250              
251 0   0       my $rule_classes = $this_config->{rules_from} || [];
252 0           my $subs = $class->find_parent_methods( $this_class, $rule_classes, FACTORY_METHOD );
253 0           my %behavior_map = ();
254 0           foreach my $sub_info ( @{ $subs } ) {
  0            
255 0           my $behavior_gen_class = $sub_info->[0];
256 0           my $behavior_gen_sub = $sub_info->[1];
257 0 0         next if ( defined $behavior_map{ $behavior_gen_class } );
258              
259             # Execute the behavior factory and map the returned
260             # information (slot => coderef or slot => \@( coderef )) into
261             # the class config.
262              
263 0   0       my $behaviors = $behavior_gen_sub->( $this_class ) || {};
264 0           $log->is_debug &&
265             $log->debug( "Behaviors returned for ($this_class): ",
266 0 0         join( ', ', keys %{ $behaviors } ) );
267 0           foreach my $slot_name ( keys %{ $behaviors } ) {
  0            
268 0           my $typeof = ref $behaviors->{ $slot_name };
269 0 0 0       next unless ( $typeof eq 'CODE' or $typeof eq 'ARRAY' );
270 0 0         $log->is_debug &&
271             $log->debug( "Adding slot behaviors for ($slot_name)" );
272 0 0         if ( $typeof eq 'CODE' ) {
    0          
273 0           push @{ $this_config->{ $PK }{behavior_table}{ $slot_name } },
  0            
274             $behaviors->{ $slot_name };
275             }
276             elsif ( $typeof eq 'ARRAY' ) {
277 0 0         next unless ( scalar @{ $behaviors->{ $slot_name } } );
  0            
278 0           push @{ $this_config->{ $PK }{behavior_table}{ $slot_name } },
  0            
279 0           @{ $behaviors->{ $slot_name } };
280             }
281 0           $behavior_map{ $behavior_gen_class }->{ $slot_name }++;
282             }
283             }
284 0           return \%behavior_map;
285             }
286              
287              
288             # Find all instances of method $method supported by classes in the ISA
289             # of $class as well as in \@added_classes. Hooray for Class::ISA!
290              
291             sub find_parent_methods {
292 0     0 0   my ( $class, $this_class, $added_classes, @method_list ) = @_;
293 0 0         return [] unless ( $this_class );
294 0 0         unless ( ref $added_classes eq 'ARRAY' ) {
295 0           $added_classes = [];
296             }
297 0           my @all_classes = ( @{ $added_classes },
  0            
298             Class::ISA::self_and_super_path( $this_class ) );
299 0           my @subs = ();
300 0           foreach my $check_class ( @all_classes ) {
301 17     17   409 no strict 'refs';
  17         208  
  17         18294  
302 0           my $src = \%{ $check_class . '::' };
  0            
303             METHOD:
304 0           foreach my $method ( @method_list ) {
305 0 0 0       if ( defined( $src->{ $method } ) and
  0            
306             defined( my $sub = *{ $src->{ $method } }{CODE} ) ) {
307 0           push @subs, [ $check_class, $sub ];
308 0 0         $log->is_debug &&
309             $log->debug( "($this_class): Found ($method) in class ($check_class)" );
310 0           last METHOD;
311             }
312             }
313             }
314 0           return \@subs;
315             }
316              
317              
318             ########################################
319             # EXECUTE BEHAVIOR
320             ########################################
321              
322              
323             # Execute behavior rules for a particular SPOPS class and slot configuration
324              
325             sub exec_behavior {
326 0     0 0   my ( $class, $slot_name, $this_class ) = @_;
327 0           my $this_config = $this_class->CONFIG;
328              
329             # Grab the behavior list and see how many there are to execute; if
330             # none, then we're all done with this slot
331              
332 0           my $behavior_list = $this_config->{ $PK }{behavior_table}{ $slot_name };
333 0 0         return 1 unless ( ref $behavior_list eq 'ARRAY' );
334 0           my $num_behaviors = scalar @{ $behavior_list };
  0            
335 0 0         return 1 unless ( $num_behaviors > 0 );
336 0 0         $log->is_debug &&
337             $log->debug( "# behaviors in ($this_class)($slot_name): $num_behaviors" );
338              
339             # Cycle through the behaviors for this slot. Note that they are
340             # currently unordered -- that is, the order shouldn't
341             # matter. (Whether this is true remains to be seen...)
342              
343 0           BEHAVIOR:
344 0           foreach my $behavior ( @{ $behavior_list } ) {
345              
346 0 0         $log->is_debug &&
347             $log->debug( "Running behavior for slot ($slot_name) and class ($this_class)" );
348              
349             # If this behavior has already been run, then skip it. This
350             # becomes relevant when we get a RESTART status from one of
351             # the behaviors (below)
352              
353 0 0         if ( $this_config->{ $PK }{behavior_run}{ $behavior } ) {
354 0 0         $log->is_debug &&
355             $log->debug( "Skipping behavior, already run." );
356 0           next BEHAVIOR;
357             }
358             # Every behavior should return a two-element list with the
359             # status and (potentially empty) message
360              
361 0           my ( $status, $msg ) = $behavior->( $this_class );
362 0 0         $log->is_info &&
363             $log->info( "Status returned from behavior: ($status)" );
364              
365 0 0         if ( $status eq ERROR ) {
366 0           SPOPS::Exception->throw( "Cannot run behavior in [$slot_name] [$this_class]: $msg" );
367             }
368              
369             # If anything but an error, go ahead and mark this behavior as
370             # run. Note that we rely on coderefs always stringifying to
371             # the same memory location. (This is a safe assumption, I think.)
372              
373 0           $this_config->{ $PK }{behavior_run}{ $behavior }++;
374              
375             # A 'DONE' means the behavior has decreed that no more
376             # processing should be done in this slot
377              
378 0 0         return 1 if ( $status eq DONE );
379              
380             # An 'OK' is normal -- either the behavior declined to do
381             # anything or did what it was supposed to do without issue
382              
383 0 0         next BEHAVIOR if ( $status eq OK );
384              
385 0 0         if ( $status eq NOTIFY ) {
386 0           warn join( "\n", "WARNING executing $slot_name for $this_config->{class}",
387             "$msg",
388             'Process will continue' ), "\n";
389 0           next BEHAVIOR;
390             }
391              
392             # RESTART is a little tricky. A 'RESTART' means that we need
393             # to re-check this class for new behaviors. If we don't find
394             # any new ones, no problem. If we do find new ones, then we
395             # need to then re-run all behavior slots before this one. Note
396             # that we will *NOT* re-run behaviors that have already been
397             # run -- we're tracking them in 'behavior_run'
398              
399 0 0         if ( $status eq RESTART ) {
400 0           $class->sync_isa( $this_class );
401 0           my $new_behavior_map = $class->find_behavior( $this_class );
402 0           my $behaviors_same = $class->compare_behavior_map(
403             $new_behavior_map,
404             $this_config->{ $PK }{behavior_map} );
405 0 0         next BEHAVIOR if ( $behaviors_same );
406 0 0         $log->is_debug &&
407             $log->debug( "Behaviors changed after receiving RESTART; re-running",
408             "from slot ($SLOTS[0]) to ($slot_name)" );
409 0           $this_config->{ $PK }{behavior_map} = $new_behavior_map;
410 0           for ( my $i = 0; $i <= $SLOT_NUM{ $slot_name }; $i++ ) {
411 0           $class->exec_behavior( $SLOTS[ $i ], $this_class );
412             }
413             }
414             }
415 0           return 1;
416             }
417              
418              
419             # Sync $this_class::ISA with $this_class->CONFIG->{isa}
420              
421             sub sync_isa {
422 0     0 0   my ( $class, $this_class ) = @_;
423 0           my $config_isa = $this_class->CONFIG->{isa};
424 17     17   123 no strict 'refs';
  17         38  
  17         12320  
425 0           @{ $this_class . '::ISA' } = @{ $config_isa };
  0            
  0            
426 0           $log->is_debug &&
427 0 0         $log->debug( "ISA for ($this_class) synched, now: ", join( ', ', @{ $config_isa } ) );
428 0           my ( $status, $msg ) = $class->require_config_classes( $this_class->CONFIG );
429 0 0         if ( $status eq ERROR ) { SPOPS::Exception->throw( $msg ) }
  0            
430 0           return 1;
431             }
432              
433              
434             # Return false if the two behavior maps don't compare (in both
435             # directions), true if they do
436              
437             sub compare_behavior_map {
438 0     0 0   my ( $class, $b1, $b2 ) = @_;
439 0 0         return undef unless ( $class->_compare_behaviors( $b1, $b2 ) );
440 0 0         return undef unless ( $class->_compare_behaviors( $b2, $b1 ) );
441 0           return 1;
442             }
443              
444              
445             # Return false if all classes and slot names of behavior-1 are not in
446             # behavior-2
447              
448             sub _compare_behaviors {
449 0     0     my ( $class, $b1, $b2 ) = @_;
450 0 0 0       return undef unless ( ref $b1 eq 'HASH' and ref $b2 eq 'HASH' );
451 0           foreach my $b1_class ( keys %{ $b1 } ) {
  0            
452 0 0         return undef unless ( $b2->{ $b1_class } );
453 0 0 0       next if ( ! $b1->{ $b1_class } and ! $b2->{ $b1_class } );
454 0 0 0       return undef if ( ref $b1->{ $b1_class } ne 'HASH' or ref $b2->{ $b1_class } ne 'HASH' );
455 0           foreach my $b1_slot_name ( keys %{ $b1->{ $b1_class } } ) {
  0            
456 0 0         return undef unless ( $b2->{ $b1_class }{ $b1_slot_name } );
457             }
458             }
459 0           return 1;
460             }
461              
462              
463             ########################################
464             # UTILITY METHODS
465             ########################################
466              
467             sub get_alias_list {
468 0     0 0   my ( $class, $all_config, $p ) = @_;
469             return [ grep ! /^_/,
470 0           ( ref $p->{alias_list} eq 'ARRAY' and scalar @{ $p->{alias_list} } )
471 0           ? @{ $p->{alias_list} }
472 0 0 0       : keys %{ $all_config } ];
473             }
474              
475             1;
476              
477             __END__