File Coverage

lib/Workflow/State.pm
Criterion Covered Total %
statement 158 186 84.9
branch 32 58 55.1
condition 2 12 16.6
subroutine 27 28 96.4
pod 13 13 100.0
total 232 297 78.1


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