File Coverage

lib/Workflow/State.pm
Criterion Covered Total %
statement 156 184 84.7
branch 32 58 55.1
condition 2 12 16.6
subroutine 27 28 96.4
pod 13 13 100.0
total 230 295 77.9


line stmt bran cond sub pod time code
1              
2             use warnings;
3 20     20   2262 use strict;
  20         34  
  20         942  
4 20     20   124 use base qw( Workflow::Base );
  20         37  
  20         535  
5 20     20   95 use Log::Log4perl qw( get_logger );
  20         52  
  20         2747  
6 20     20   122 use Workflow::Condition;
  20         29  
  20         156  
7 20     20   1822 use Workflow::Condition::Evaluate;
  20         55  
  20         223  
8 20     20   6754 use Workflow::Exception qw( workflow_error condition_error );
  20         63  
  20         130  
9 20     20   916 use Exception::Class;
  20         41  
  20         876  
10 20     20   132 use Workflow::Factory qw( FACTORY );
  20         38  
  20         201  
11 20     20   1814 use English qw( -no_match_vars );
  20         39  
  20         143  
12 20     20   666  
  20         44  
  20         83  
13             $Workflow::State::VERSION = '1.60';
14              
15             my @FIELDS = qw( state description type );
16             my @INTERNAL = qw( _test_condition_count _factory _actions _conditions
17             _next_state );
18             __PACKAGE__->mk_accessors( @FIELDS, @INTERNAL );
19              
20              
21             ########################################
22             # PUBLIC
23              
24             my ( $self, $action_name ) = @_;
25             $self->_contains_action_check($action_name);
26 139     139 1 302 return @{ $self->_conditions->{$action_name} };
27 139         412 }
28 139         1469  
  139         336  
29             my ( $self, $wf, $action_name ) = @_;
30             my $common_config =
31             $self->_factory->get_action_config($wf, $action_name);
32 60     60 1 143 my $state_config = $self->_actions->{$action_name};
33 60         241 my $config = { %{$common_config}, %{$state_config} };
34             my $action_class = $common_config->{class};
35 60         133  
36 60         495 return $action_class->new( $wf, $config );
  60         229  
  60         351  
37 60         151 }
38              
39 60         629 my ( $self, $action_name ) = @_;
40             return $self->_actions->{$action_name};
41             }
42              
43 252     252 1 405 my ($self) = @_;
44 252         910 return keys %{ $self->_actions };
45             }
46              
47             my ( $self, $wf, $group ) = @_;
48 29     29 1 59 my @all_actions = $self->get_all_action_names;
49 29         44 my @available_actions = ();
  29         104  
50              
51             # assuming that the user wants the _fresh_ list of available actions,
52             # we clear the condition cache before checking which ones are available
53 29     29 1 8991 local $wf->{'_condition_result_cache'} = {};
54 29         98  
55 29         413 foreach my $action_name (@all_actions) {
56              
57             if ( $group ) {
58             my $action_config =
59 29         82 $self->_factory()->get_action_config( $wf, $action_name );
60             if ( defined $action_config->{group}
61 29         72 and $action_config->{group} ne $group ) {
62             next;
63 76 50       534 }
64 0         0 }
65              
66 0 0 0     0 if ( $self->is_action_available( $wf, $action_name ) ) {
67             push @available_actions, $action_name;
68 0         0 }
69             }
70             return @available_actions;
71             }
72 76 100       218  
73 43         117 my ( $self, $wf, $action_name ) = @_;
74             eval { $self->evaluate_action( $wf, $action_name ) };
75              
76 29         329 # Everything is fine
77             return 1 unless( $EVAL_ERROR );
78              
79             # We got an exception, check if it is a Workflow::Exception
80 76     76 1 156 return 0 if (Exception::Class->caught('Workflow::Exception'));
81 76         110  
  76         520  
82             $EVAL_ERROR->rethrow() if (ref $EVAL_ERROR);
83              
84 76 100       24805 croak $EVAL_ERROR;
85             }
86              
87 33 50       309 my ($self) = @_;
88             return; # left for backward compatibility with 1.49
89 0 0       0 }
90              
91 0         0 my ( $self, $wf, $action_name ) = @_;
92             my $state = $self->state;
93              
94             # NOTE: this will throw an exception if C<$action_name> is not
95 0     0 1 0 # contained in this state, so there's no need to do it explicitly
96 0         0  
97             my @conditions = $self->get_conditions($action_name);
98             foreach my $condition (@conditions) {
99             my $condition_name = $condition->name;
100 136     136 1 356  
101 136         468 my $rv;
102             eval {
103             $rv = Workflow::Condition->evaluate_condition($wf, $condition_name);
104             };
105             if ($EVAL_ERROR) {
106 136         1662 if (Exception::Class->caught('Workflow::Exception::Condition')) {
107 136         1390 condition_error "No access to action '$action_name' in ",
108 84         417 "state '$state' because $EVAL_ERROR ";
109             }
110 84         901 else {
111 84         105 $EVAL_ERROR->rethrow() if (ref $EVAL_ERROR ne '');
112 84         278 # For briefness, we just send back the first line of EVAL
113             my @t = split /\n/, $EVAL_ERROR;
114 84 50       736 my $ee = shift @t;
    100          
115 0 0       0 Exception::Class::Base->throw(
116 0         0 error
117             => "Got unknown exception while handling condition '$condition_name' / " . $ee );
118             }
119             }
120 0 0       0 elsif (! $rv) {
121             condition_error "No access to action '$action_name' in ",
122 0         0 "state '$state' because condition '$condition_name' failed";
123 0         0 }
124 0         0 }
125             }
126              
127             my ( $self, $action_name, $action_return ) = @_;
128             $self->_contains_action_check($action_name);
129             my $resulting_state = $self->_next_state->{$action_name};
130 33         239 return $resulting_state unless ( ref($resulting_state) eq 'HASH' );
131              
132             if ( defined $action_return ) {
133              
134             # TODO: Throw exception if $action_return not found and no '*' defined?
135             return $resulting_state->{$action_return} || $resulting_state->{'*'};
136             } else {
137 53     53 1 134 return %{$resulting_state};
138 53         151 }
139 53         777 }
140 53 50       655  
141             my ( $self, $wf ) = @_;
142 0 0       0 my $state = $self->state;
143             unless ( $self->autorun ) {
144             workflow_error "State '$state' is not marked for automatic ",
145 0   0     0 "execution. If you want it to be run automatically ",
146             "set the 'autorun' property to 'yes'.";
147 0         0 }
  0         0  
148              
149             my @actions = $self->get_available_action_names($wf);
150             my $pre_error = "State '$state' should be automatically executed but";
151             if ( scalar @actions > 1 ) {
152 9     9 1 21 workflow_error "$pre_error there are multiple actions available ",
153 9         31 "for execution. Actions are: ", join ', ', @actions;
154 9 50       102 }
155 0         0 if ( scalar @actions == 0 ) {
156             workflow_error
157             "$pre_error there are no actions available for execution.";
158             }
159             $self->log->debug("Auto-running state '$state' with action '$actions[0]'");
160 9         39 return $actions[0];
161 9         33 }
162 9 50       32  
163 0         0 my ( $self, $setting ) = @_;
164             if ( defined $setting ) {
165             if ( $setting =~ /^(true|1|yes)$/i ) {
166 9 50       31 $self->{autorun} = 'yes';
167 0         0 } else {
168             $self->{autorun} = 'no';
169             }
170 9         50 }
171 9         94 return ( $self->{autorun} eq 'yes' );
172             }
173              
174             my ( $self, $setting ) = @_;
175 221     221 1 402 if ( defined $setting ) {
176 221 100       480 if ( $setting =~ /^(true|1|yes)$/i ) {
177 138 100       696 $self->{may_stop} = 'yes';
178 9         14 } else {
179             $self->{may_stop} = 'no';
180 129         219 }
181             }
182             return ( $self->{may_stop} eq 'yes' );
183 221         674 }
184              
185             ########################################
186             # INTERNAL
187 138     138 1 208  
188 138 50       247 my ( $self, $config, $factory ) = @_;
189 138 50       313  
190 0         0 # Fallback for old style
191             $factory ||= FACTORY;
192 138         223 my $name = $config->{name};
193              
194             my $class = ref $self;
195 138         182  
196             $self->log->debug("Constructing '$class' object for state $name");
197              
198             $self->state($name);
199             $self->_factory($factory);
200             $self->_actions( {} );
201             $self->_conditions( {} );
202 138     138 1 246 $self->_next_state( {} );
203              
204             # Note this is the workflow type.
205 138   33     294 $self->type( $config->{type} );
206 138         242 $self->description( $config->{description} );
207              
208 138         197 if ( $config->{autorun} ) {
209             $self->autorun( $config->{autorun} );
210 138         394 } else {
211             $self->autorun('no');
212 138         44714 }
213 138         1958 if ( $config->{may_stop} ) {
214 138         1241 $self->may_stop( $config->{may_stop} );
215 138         1291 } else {
216 138         1345 $self->may_stop('no');
217             }
218             foreach my $state_action_config ( @{ $config->{action} } ) {
219 138         1241 my $action_name = $state_action_config->{name};
220 138         1431 $self->log->debug("Adding action '$action_name' to '$class' '$name'");
221             $self->_add_action_config( $action_name, $state_action_config );
222 138 100       1212 }
223 9         16 }
224              
225 129         419 my ( $self, $action_name, $resulting ) = @_;
226             my $name = $self->state;
227 138 50       254 my @errors = ();
228 0         0 my %new_resulting = ();
229             foreach my $map ( @{$resulting} ) {
230 138         254 if ( not $map->{state} or not defined $map->{return} ) {
231             push @errors,
232 138         143 "Must have both 'state' ($map->{state}) and 'return' "
  138         404  
233 153         505 . "($map->{return}) keys defined.";
234 153         320 } elsif ( $new_resulting{ $map->{return} } ) {
235 153         33824 push @errors, "The 'return' value ($map->{return}) must be "
236             . "unique among the resulting states.";
237             } else {
238             $new_resulting{ $map->{return} } = $map->{state};
239             }
240 10     10   36 }
241 10         32 if ( scalar @errors ) {
242 10         96 workflow_error "Errors found assigning 'resulting_state' to ",
243 10         23 "action '$action_name' in state '$name': ", join '; ', @errors;
244 10         20 }
  10         49  
245 20 50 33     163 $self->log->debug( "Assigned multiple resulting states in '$name' and ",
    50          
246 0         0 "action '$action_name' from array ok" );
247             return \%new_resulting;
248             }
249              
250 0         0 my ( $self, $action_name, $resulting ) = @_;
251              
252             if ( my $resulting_type = ref $resulting ) {
253 20         85 if ( $resulting_type eq 'ARRAY' ) {
254             $resulting
255             = $self->_assign_next_state_from_array( $action_name,
256 10 50       51 $resulting );
257 0         0 }
258             }
259              
260 10         52 return $resulting;
261             }
262 10         2931  
263             my ( $self, $action_name, $action_config ) = @_;
264             my $state = $self->state;
265             unless ( $action_config->{resulting_state} ) {
266 153     153   230 my $no_change_value = Workflow->NO_CHANGE_VALUE;
267             workflow_error "Action '$action_name' in state '$state' does not ",
268 153 100       659 "have the key 'resulting_state' defined. This key ",
269 10 50       81 "is required -- if you do not want the state to ",
270 10         43 "change, use the value '$no_change_value'.";
271             }
272             # Copy the action config,
273             # so we can delete keys consumed by the state below
274             my $copied_config = { %$action_config };
275             my $resulting_state = delete $copied_config->{resulting_state};
276 153         331 my $condition = delete $copied_config->{condition};
277              
278             # Removes 'resulting_state' key from action_config
279             $self->_next_state->{$action_name} =
280 153     153   296 $self->_create_next_state( $action_name, $resulting_state );
281 153         429  
282 153 50       1812 # Removes 'condition' key from action_config
283 0         0 $self->_conditions->{$action_name} = [
284 0         0 $self->_create_condition_objects( $action_name, $condition )
285             ];
286              
287             $self->_actions->{$action_name} = $copied_config;
288             }
289              
290             my ( $self, $action_name, $action_conditions ) = @_;
291 153         620 my @conditions = $self->normalize_array( $action_conditions );
292 153         274 my @condition_objects = ();
293 153         202 my $count = 1;
294             foreach my $condition_info (@conditions) {
295              
296 153         292 # Special case: a 'test' denotes our 'evaluate' condition
297             if ( $condition_info->{test} ) {
298             my $state = $self->state();
299             push @condition_objects,
300 153         1452 Workflow::Condition::Evaluate->new(
301             { name => "_$state\_$action_name\_condition\_$count",
302             class => 'Workflow::Condition::Evaluate',
303             test => $condition_info->{test},
304 153         1431 }
305             );
306             $count++;
307             } else {
308 153     153   218 $self->log->info(
309 153         379 "Fetching condition '$condition_info->{name}'");
310 153         180 push @condition_objects,
311 153         173 $self->_factory()
312 153         236 ->get_condition( $condition_info->{name}, $self->type() );
313             }
314             }
315 88 100       197 return @condition_objects;
316 16         47 }
317              
318             my ( $self, $action_name ) = @_;
319             unless ( $self->contains_action($action_name) ) {
320             workflow_error "State '", $self->state, "' does not contain ",
321             "action '$action_name'";
322             }
323 16         398 }
324 16         55  
325             1;
326 72         145  
327              
328             =pod
329              
330 72         15549 =head1 NAME
331              
332             Workflow::State - Information about an individual state in a workflow
333 153         402  
334             =head1 VERSION
335              
336             This documentation describes version 1.60 of this package
337 192     192   406  
338 192 50       461 =head1 SYNOPSIS
339 0            
340             # This is an internal object...
341             <workflow...>
342             <state name="Start">
343             <action ... resulting_state="Progress" />
344             </state>
345             ...
346             <state name="Progress" description="I am in progress">
347             <action ... >
348             <resulting_state return="0" state="Needs Affirmation" />
349             <resulting_state return="1" state="Approved" />
350             <resulting_state return="*" state="Needs More Info" />
351             </action>
352             </state>
353             ...
354             <state name="Approved" autorun="yes">
355             <action ... resulting_state="Completed" />
356             ...
357              
358             =head1 DESCRIPTION
359              
360             Each L<Workflow::State> object represents a state in a workflow. Each
361             state can report its name, description and all available
362             actions. Given the name of an action it can also report what
363             conditions are attached to the action and what state will result from
364             the action (the 'resulting state').
365              
366             =head2 Resulting State
367              
368             The resulting state is action-dependent. For instance, in the
369             following example you can perform two actions from the state 'Ticket
370             Created' -- 'add comment' and 'edit issue':
371              
372             <state name="Ticket Created">
373             <action name="add comment"
374             resulting_state="NOCHANGE" />
375             <action name="edit issue"
376             resulting_state="Ticket In Progress" />
377             </state>
378              
379             If you execute 'add comment' the new state of the workflow will be the
380             same ('NOCHANGE' is a special state). But if you execute 'edit issue'
381             the new state will be 'Ticket In Progress'.
382              
383             You can also have multiple return states for a single action. The one
384             chosen by the workflow system will depend on what the action
385             returns. For instance we might have something like:
386              
387             <state name="create user">
388             <action name="create">
389             <resulting_state return="admin" state="Assign as Admin" />
390             <resulting_state return="helpdesk" state="Assign as Helpdesk" />
391             <resulting_state return="*" state="Assign as Luser" />
392             </action>
393             </state>
394              
395             So if we execute 'create' the workflow will be in one of three states:
396             'Assign as Admin' if the return value of the 'create' action is
397             'admin', 'Assign as Helpdesk' if the return is 'helpdesk', and 'Assign
398             as Luser' if the return is anything else.
399              
400             =head2 Autorun State
401              
402             You can also indicate that the state should be automatically executed
403             when the workflow enters it using the 'autorun' property. Note the
404             slight change in terminology -- typically we talk about executing an
405             action, not a state. But we can use both here because an automatically
406             run state requires that one and only one action is available for
407             running. That doesn't mean a state contains only one action. It just
408             means that only one action is available when the state is entered. For
409             example, you might have two actions with mutually exclusive conditions
410             within the autorun state.
411              
412             If no action or more than one action is available at the time the
413             workflow enters an autorun state, Workflow will throw an error. There
414             are some conditions where this might not be what you want. For example
415             when you have a state which contains an action that depends on some
416             condition. If it is true, you might be happy to move on to the next
417             state, but if it is not, you are fine to come back and try again later
418             if the action is available. This behaviour can be achived by setting the
419             'may_stop' property to yes, which will cause Workflow to just quietly
420             stop automatic execution if it does not have a single action to execute.
421              
422             =head1 PUBLIC METHODS
423              
424             =head3 get_conditions( $action_name )
425              
426             Returns a list of L<Workflow::Condition> objects for action
427             C<$action_name>. Throws exception if object does not contain
428             C<$action_name> at all.
429              
430             =head3 get_action( $workflow, $action_name )
431              
432             Returns an L<Workflow::Action> instance initialized using both the
433             global configuration provided to the named action in the "action
434             configuration" provided to the factory as well as any configuration
435             specified as part of the listing of actions in the state of the
436             workflow declaration.
437              
438             =head3 contains_action( $action_name )
439              
440             Returns true if this state contains action C<$action_name>, false if
441             not.
442              
443             =head3 is_action_available( $workflow, $action_name )
444              
445             Returns true if C<$action_name> is contained within this state B<and>
446             it matches any conditions attached to it, using the data in the
447             context of the C<$workflow> to do the checks.
448              
449             =head3 evaluate_action( $workflow, $action_name )
450              
451             Throws exception if action C<$action_name> is either not contained in
452             this state or if it does not pass any of the attached conditions,
453             using the data in the context of C<$workflow> to do the checks.
454              
455             =head3 get_all_action_names()
456              
457             Returns list of all action names available in this state.
458              
459             =head3 get_available_action_names( $workflow, $group )
460              
461             Returns all actions names that are available given the data in
462             C<$workflow>. Each action name returned will return true from
463             B<is_action_available()>.
464             $group is optional parameter. If it is set, additional check for group
465             membership will be performed.
466              
467             =head3 get_next_state( $action_name, [ $action_return ] )
468              
469             Returns the state(s) that will result if action C<$action_name>
470             is executed. If you've specified multiple return states in the
471             configuration then you need to specify the C<$action_return>,
472             otherwise we return a hash with action return values as the keys and
473             the action names as the values.
474              
475             =head3 get_autorun_action_name( $workflow )
476              
477             Retrieve the action name to be autorun for this state. If the state
478             does not have the 'autorun' property enabled this throws an
479             exception. It also throws an exception if there are multiple actions
480             available or if there are no actions available.
481              
482             Returns name of action to be used for autorunning the state.
483              
484             =head3 clear_condition_cache ( )
485              
486             Deprecated, kept for 1.60 compatibility.
487              
488             Used to empties the condition result cache for a given state.
489              
490             =head1 PROPERTIES
491              
492             All property methods act as a getter and setter. For example:
493              
494             my $state_name = $state->state;
495             $state->state( 'some name' );
496              
497             B<state>
498              
499             Name of this state (required).
500              
501             B<description>
502              
503             Description of this state (optional).
504              
505             =head3 autorun
506              
507             Returns true if the state should be automatically run, false if
508             not. To set to true the property value should be 'yes', 'true' or 1.
509              
510             =head3 may_stop
511              
512             Returns true if the state may stop automatic execution silently, false
513             if not. To set to true the property value should be 'yes', 'true' or 1.
514              
515             =head1 INTERNAL METHODS
516              
517             =head3 init( $config )
518              
519             Assigns 'state', 'description', 'autorun' and 'may_stop' properties from
520             C<$config>. Also assigns configuration for all actions in the state,
521             performing some sanity checks like ensuring every action has a
522             'resulting_state' key.
523              
524             =head1 SEE ALSO
525              
526             =over
527              
528             =item * L<Workflow>
529              
530             =item * L<Workflow::Condition>
531              
532             =item * L<Workflow::Factory>
533              
534             =back
535              
536             =head1 COPYRIGHT
537              
538             Copyright (c) 2003-2022 Chris Winters. All rights reserved.
539              
540             This library is free software; you can redistribute it and/or modify
541             it under the same terms as Perl itself.
542              
543             Please see the F<LICENSE>
544              
545             =head1 AUTHORS
546              
547             Please see L<Workflow>
548              
549             =cut