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   2343 use strict;
  20         33  
  20         671  
4 20     20   94 use base qw( Workflow::Base );
  20         42  
  20         511  
5 20     20   90 use Log::Log4perl qw( get_logger );
  20         36  
  20         2370  
6 20     20   120 use Workflow::Condition;
  20         36  
  20         130  
7 20     20   1656 use Workflow::Condition::Evaluate;
  20         34  
  20         189  
8 20     20   6560 use Workflow::Exception qw( workflow_error condition_error );
  20         45  
  20         102  
9 20     20   820 use Exception::Class;
  20         29  
  20         792  
10 20     20   101 use Workflow::Factory qw( FACTORY );
  20         32  
  20         153  
11 20     20   1675 use English qw( -no_match_vars );
  20         42  
  20         116  
12 20     20   639  
  20         47  
  20         82  
13             $Workflow::State::VERSION = '1.61';
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 239 return @{ $self->_conditions->{$action_name} };
27 139         300 }
28 139         1277  
  139         241  
29             my ( $self, $wf, $action_name ) = @_;
30             my $common_config =
31             $self->_factory->get_action_config($wf, $action_name);
32 60     60 1 139 my $state_config = $self->_actions->{$action_name};
33 60         153 my $config = { %{$common_config}, %{$state_config} };
34             my $action_class = $common_config->{class};
35 60         120  
36 60         487 return $action_class->new( $wf, $config );
  60         156  
  60         292  
37 60         130 }
38              
39 60         462 my ( $self, $action_name ) = @_;
40             return $self->_actions->{$action_name};
41             }
42              
43 252     252 1 357 my ($self) = @_;
44 252         480 return keys %{ $self->_actions };
45             }
46              
47             my ( $self, $wf, $group ) = @_;
48 29     29 1 42 my @all_actions = $self->get_all_action_names;
49 29         30 my @available_actions = ();
  29         70  
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 6857 local $wf->{'_condition_result_cache'} = {};
54 29         60  
55 29         348 foreach my $action_name (@all_actions) {
56              
57             if ( $group ) {
58             my $action_config =
59 29         67 $self->_factory()->get_action_config( $wf, $action_name );
60             if ( defined $action_config->{group}
61 29         53 and $action_config->{group} ne $group ) {
62             next;
63 76 50       399 }
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       167  
73 43         94 my ( $self, $wf, $action_name ) = @_;
74             eval { $self->evaluate_action( $wf, $action_name ) };
75              
76 29         290 # 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 141 return 0 if (Exception::Class->caught('Workflow::Exception'));
81 76         85  
  76         439  
82             $EVAL_ERROR->rethrow() if (ref $EVAL_ERROR);
83              
84 76 100       22827 croak $EVAL_ERROR;
85             }
86              
87 33 50       206 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 214  
101 136         289 my $rv;
102             eval {
103             $rv = Workflow::Condition->evaluate_condition($wf, $condition_name);
104             };
105             if ($EVAL_ERROR) {
106 136         1318 if (Exception::Class->caught('Workflow::Exception::Condition')) {
107 136         1236 condition_error "No access to action '$action_name' in ",
108 84         237 "state '$state' because $EVAL_ERROR ";
109             }
110 84         748 else {
111 84         92 $EVAL_ERROR->rethrow() if (ref $EVAL_ERROR ne '');
112 84         192 # For briefness, we just send back the first line of EVAL
113             my @t = split /\n/, $EVAL_ERROR;
114 84 50       639 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         153 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 98 return %{$resulting_state};
138 53         121 }
139 53         567 }
140 53 50       545  
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         14 "for execution. Actions are: ", join ', ', @actions;
154 9 50       77 }
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         22 return $actions[0];
161 9         28 }
162 9 50       19  
163 0         0 my ( $self, $setting ) = @_;
164             if ( defined $setting ) {
165             if ( $setting =~ /^(true|1|yes)$/i ) {
166 9 50       17 $self->{autorun} = 'yes';
167 0         0 } else {
168             $self->{autorun} = 'no';
169             }
170 9         32 }
171 9         70 return ( $self->{autorun} eq 'yes' );
172             }
173              
174             my ( $self, $setting ) = @_;
175 221     221 1 345 if ( defined $setting ) {
176 221 100       435 if ( $setting =~ /^(true|1|yes)$/i ) {
177 138 100       590 $self->{may_stop} = 'yes';
178 9         12 } else {
179             $self->{may_stop} = 'no';
180 129         207 }
181             }
182             return ( $self->{may_stop} eq 'yes' );
183 221         561 }
184              
185             ########################################
186             # INTERNAL
187 138     138 1 198  
188 138 50       212 my ( $self, $config, $factory ) = @_;
189 138 50       317  
190 0         0 # Fallback for old style
191             $factory ||= FACTORY;
192 138         194 my $name = $config->{name};
193              
194             my $class = ref $self;
195 138         166  
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 209 $self->_next_state( {} );
203              
204             # Note this is the workflow type.
205 138   33     243 $self->type( $config->{type} );
206 138         189 $self->description( $config->{description} );
207              
208 138         183 if ( $config->{autorun} ) {
209             $self->autorun( $config->{autorun} );
210 138         342 } else {
211             $self->autorun('no');
212 138         44414 }
213 138         1724 if ( $config->{may_stop} ) {
214 138         1253 $self->may_stop( $config->{may_stop} );
215 138         1229 } else {
216 138         1195 $self->may_stop('no');
217             }
218             foreach my $state_action_config ( @{ $config->{action} } ) {
219 138         1176 my $action_name = $state_action_config->{name};
220 138         1366 $self->log->debug("Adding action '$action_name' to '$class' '$name'");
221             $self->_add_action_config( $action_name, $state_action_config );
222 138 100       1172 }
223 9         13 }
224              
225 129         346 my ( $self, $action_name, $resulting ) = @_;
226             my $name = $self->state;
227 138 50       232 my @errors = ();
228 0         0 my %new_resulting = ();
229             foreach my $map ( @{$resulting} ) {
230 138         227 if ( not $map->{state} or not defined $map->{return} ) {
231             push @errors,
232 138         162 "Must have both 'state' ($map->{state}) and 'return' "
  138         313  
233 153         481 . "($map->{return}) keys defined.";
234 153         308 } elsif ( $new_resulting{ $map->{return} } ) {
235 153         33540 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   28 }
241 10         29 if ( scalar @errors ) {
242 10         90 workflow_error "Errors found assigning 'resulting_state' to ",
243 10         20 "action '$action_name' in state '$name': ", join '; ', @errors;
244 10         17 }
  10         27  
245 20 50 33     124 $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         79 if ( $resulting_type eq 'ARRAY' ) {
254             $resulting
255             = $self->_assign_next_state_from_array( $action_name,
256 10 50       35 $resulting );
257 0         0 }
258             }
259              
260 10         36 return $resulting;
261             }
262 10         2978  
263             my ( $self, $action_name, $action_config ) = @_;
264             my $state = $self->state;
265             unless ( $action_config->{resulting_state} ) {
266 153     153   228 my $no_change_value = Workflow->NO_CHANGE_VALUE;
267             workflow_error "Action '$action_name' in state '$state' does not ",
268 153 100       309 "have the key 'resulting_state' defined. This key ",
269 10 50       91 "is required -- if you do not want the state to ",
270 10         48 "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   281 $self->_create_next_state( $action_name, $resulting_state );
281 153         313  
282 153 50       1672 # 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         581 my @conditions = $self->normalize_array( $action_conditions );
292 153         257 my @condition_objects = ();
293 153         189 my $count = 1;
294             foreach my $condition_info (@conditions) {
295              
296 153         299 # Special case: a 'test' denotes our 'evaluate' condition
297             if ( $condition_info->{test} ) {
298             my $state = $self->state();
299             push @condition_objects,
300 153         1384 Workflow::Condition::Evaluate->new(
301             { name => "_$state\_$action_name\_condition\_$count",
302             class => 'Workflow::Condition::Evaluate',
303             test => $condition_info->{test},
304 153         1339 }
305             );
306             $count++;
307             } else {
308 153     153   208 $self->log->info(
309 153         331 "Fetching condition '$condition_info->{name}'");
310 153         182 push @condition_objects,
311 153         155 $self->_factory()
312 153         217 ->get_condition( $condition_info->{name}, $self->type() );
313             }
314             }
315 88 100       182 return @condition_objects;
316 16         64 }
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         365 }
324 16         57  
325             1;
326 72         136  
327              
328             =pod
329              
330 72         15532 =head1 NAME
331              
332             Workflow::State - Information about an individual state in a workflow
333 153         419  
334             =head1 VERSION
335              
336             This documentation describes version 1.61 of this package
337 192     192   276  
338 192 50       329 =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.61 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