#### # Functions implementing workflow:* processing #### # # need to load workflow definitions during server startup # -- tie into XSM initialization/configuration # # or we can use the workflow:$path as the name of the workflow and load on demand # # still need to manage conditions, etc. # # # $ob = workflow:create('type'); # $ob = workflow:fetch('type', $id); # @ids = workflow:find('type', 'user', 'state') # any that are blank/undef or '*' are not used as criteria # @available_actions = $ob/current-actions # @fields = $ob/method::get-action-fields( $action ) # @required_fields = $ob/method::get-action-fields( $action )[is-required = 1] # ## do we want to be able to take the get-action-fields data and use it in the statemachine? # $context = $ob/context # $context/method::param( $field_name, $data ) # $ob/method::execute-action( $action ) # $state = $ob/state # # ## set $ob/context/method::param('user', $user_id) in the workflow:create() and workflow:fetch() calls # ## (assuming this isn't saved unless there is a change in state) # ## need to be able to assign ownership -- might be able to through acl system, but not the best # # $id = $ob/id # $type = $ob/type # $description = $ob/description # # to be supported by Workflow::Persister::Gestinanna : # need to be able to search based on user / type / state # database can support search on type and state # need to add user field that indicates owner / initial creator # # need to allow script elements in condition and action definitions -- # may need to refactor compiler a slight bit to allow this # - make script elements a different namespace -- should solve most of it # then just compile with or root element instead of # this needs a little more thought # package Gestinanna::XSM::Workflow; use base qw(Gestinanna::XSM); use strict; use Gestinanna::XSM::Expression; #our @ISA = qw(Gestinanna::XSM); our $NS = 'http://ns.gestinanna.org/workflow'; sub start_document { return "#initialize workflow namespace\n"; } sub end_document { return ''; } sub comment { return ''; } sub processing_instruction { return ''; } sub manage_text { 1 } sub characters { my ($e, $text) = @_; return $text -> {Data}; $e -> append_state('script', $text); return ''; } sub start_element { my ($e, $node) = @_; my ($tag, %attribs); $tag = $node->{Name}; foreach my $attrib (@{$node->{Attributes}}) { $attribs{$attrib->{Name}} = $attrib->{Value}; } warn "start $tag: ", join(", ", map { "$_ => $attribs{$_}" } keys %attribs), "\n"; if ($tag eq 'workflow') { # root of workflow definition } elsif ($tag eq 'inherit') { # need to load inherited definition and overwrite with new stuff } elsif( $tag eq 'state') { $e -> push_state; $e -> set_state('description', ''); } elsif( $tag eq 'description') { $e -> push_state; $e -> set_state('text', ''); } elsif( $tag eq 'action') { if($attribs{'id'}) { $e -> push_state; $e -> reset_state('in-expression'); $e -> set_state('script', ''); $e -> enter_state('workflow-action'); $e -> {WF_CONDITIONS} = [ ]; } else { $e -> {WF_CONDITIONS} = [ ]; } } elsif( $tag eq 'condition') { if($attribs{'xref'}) { push @{$e -> {WF_CONDITIONS} ||= []}, $attribs{'xref'}; } elsif($attribs{'id'}) { $e -> push_state; $e -> reset_state('in-expression'); $e -> set_state('script', ''); } } elsif( $tag eq 'validator') { if($attribs{'xref'}) { push @{$e -> {WF_VALIDATORS} ||= []}, $attribs{'xref'}; } elsif($attribs{'id'}) { $e -> push_state; $e -> reset_state('in-expression'); $e -> set_state('script', ''); } } elsif( $tag eq 'param') { $e -> push_state; $e -> set_state('init', ''); } elsif( $tag eq 'field') { $e -> push_state; $e -> reset_state('in-expression'); $e -> set_state('script', ''); } elsif( $tag eq 'add-context' ) { $e -> enter_state('in-expression'); $e -> enter_state('in-list'); my $wf = Gestinanna::XSM::compile_expr($e, $attribs{workflow}); my $select = $attribs{select}; $select = '' unless defined $select; $select = Gestinanna::XSM::compile_expr($e, $select) . ", " if $select ne ''; return "Gestinanna::XSM::Workflow::xsm_add_context(\$sm, $wf, [ $select"; } elsif( $tag eq 'choose-action' ) { $e -> push_state; $e -> reset_state('script'); $e -> reset_state('workflow-action-otherwise'); $e -> enter_state('in-workflow-action'); my $wfv = '$wf' . $e -> state('in-workflow-action'); my $wf = Gestinanna::XSM::compile_expr($e, $attribs{workflow}); if($e -> state('in-expression')) { return "( (my $wfv = $wf, ("; } return "my $wfv = $wf;\nif(0) { "; } elsif( $tag eq 'when' ) { my $wfv = '$wf' . $e -> state('in-workflow-action'); if($e -> state('in-expression')) { return "(Gestinanna::XSM::Workflow::execute_action($wfv, " . Gestinanna::XSM::static_expr($e, $attribs{action}) . ") ? (\n"; } return " } elsif(Gestinanna::XSM::Workflow::execute_action($wfv, " . Gestinanna::XSM::static_expr($e, $attribs{action}) . ")) {\n"; } elsif( $tag eq 'otherwise' ) { $e -> push_state; $e -> reset_state('script'); return ''; } elsif( $tag eq 'add-history' ) { my $wf = $attribs{workflow}; if(defined $wf) { $wf = Gestinanna::XSM::compile_expr($e, $wf); } elsif($e -> state('in-workflow-action')) { $wf = '$wf' . $e -> state('in-workflow-action'); } elsif($e -> state('workflow-action')) { $wf = '$wf'; } else { $wf = "undef"; } $e -> push_state; $e -> enter_state('in-association'); $e -> reset_state('in-expression'); my $a = '%a' . $e -> state('in-association'); return "Gestinanna::XSM::Workflow::add_history($wf, do { my $a;"; } else { warn("Unrecognised tag: $tag"); } return ''; } sub end_element { my ($e, $node) = @_; my $tag = $node->{Name}; my (%attribs); foreach my $attrib (@{$node->{Attributes}}) { $attribs{$attrib->{Name}} = $attrib->{Value}; } warn "end $tag: ", join(", ", map { "$_ => $attribs{$_}" } keys %attribs), "\n"; if ($tag eq 'workflow') { my $script_start = $e -> get_script_start; my $script_end = $e -> get_script_end; my $data = { workflow => { state => $e -> {WF_STATE} || [], description => $e -> state('description') || '', persister => $attribs{'persister'} || '', }, condition => $e -> {WF_DEF_CONDITIONS} || {}, validator => $e -> {WF_DEF_VALIDATORS} || {}, action => $e -> {WF_DEF_ACTIONS} || {}, }; my(%action_map, %condition_map, %validator_map); foreach my $parent (reverse @{$e -> {WF_PARENTS} || []}) { my %a = $parent -> action_map; @action_map{keys %a} = values %a; %a = $parent -> condition_map; @condition_map{keys %a} = values %a; %a = $parent -> validator_map; @validator_map{keys %a} = values %a; } warn Data::Dumper -> Dump([$data]); # __PACKAGE__::Actions::$Action = package for $Action # __PACKAGE__::Conditions::$Condition # ... # loaded at server startup - not on-demand # by taglib # # # # # # # # foreach my $a (@{$data -> {action} || []}) { $action_map{$a -> {name}} = qq{__PACKAGE__ . "::Actions::$$a{name}"}; } foreach my $c (@{$data -> {condition} || []}) { $condition_map{$c -> {name}} = qq{__PACKAGE__."::Conditions::$$c{name}"}; } #foreach my $v (@{$data -> {validator} || []}) { #$validator_map{$v -> {name}} = \__PACKAGE__."::Validators::$$v{name}"; #} my $code = ''; my $pkg_code = ''; $code .= <<1HERE1; use base qw(Gestinanna::Workflow); use vars qw(\%WORKFLOW \%ACTION_MAP \%CONDITION_MAP \%VALIDATOR_MAP \%ACTIONS \%CONDITIONS \%VALIDATORS); 1HERE1 $code .= "\%ACTION_MAP = (\n " . join(",\n ", map { qq{$_ => $action_map{$_}} } keys %action_map) . "\n);\n\n"; $code .= "\%CONDITION_MAP = (\n " . join(",\n ", map { qq{$_ => $condition_map{$_}} } keys %condition_map) . "\n);\n\n"; $code .= "\%VALIDATOR_MAP = (\n " . join(",\n ", map { qq{$_ => $validator_map{$_}} } keys %validator_map) . "\n);\n\n"; my $description = Gestinanna::XSM::makeSingleQuoted($data -> {workflow} -> {description}); my $persister = Gestinanna::XSM::makeSingleQuoted($data -> {workflow} -> {persister}); $code .= <<1HERE1; \%WORKFLOW = ( description => $description, persister => $persister, state => [ 1HERE1 foreach my $state (@{$data -> {workflow} -> {state} || []}) { $description = Gestinanna::XSM::makeSingleQuoted($state -> {description}); my $name = Gestinanna::XSM::makeSingleQuoted($state -> {name}); $code .= <<1HERE1; { name => $name, description => $description, action => [ 1HERE1 foreach my $action(@{$state -> {action} || []}) { $name = $action_map{$action -> {name}}; #$name = Gestinanna::XSM::makeSingleQuoted($action -> {name}); my $resulting_state = Gestinanna::XSM::makeSingleQuoted($action -> {resulting_state}); $code .= <<1HERE1; { name => $name, resulting_state => $resulting_state, 1HERE1 $code .= " condition => [" . join(", ", map { qq{{ name => \__PACKAGE__."::Conditions::${_}"}} } @{$action -> {condition} || []}) . "],\n"; $code .= <<1HERE1; }, 1HERE1 } $code .= <<1HERE1; ], }, 1HERE1 } $code .= " ],\n);\n"; $code .= "\n%ACTIONS = (\n"; foreach my $action (@{$data -> {action} || []}) { my $package = $action_map{$action -> {name}}; my $name = qq{$package}; if(0 && $package =~ m{^__PACKAGE__}) { $package =~ s{^__PACKAGE__\s*\.\s*"}{}; $package =~ s{"$}{}; $package = "__PACKAGE__$package"; } $pkg_code .= <<1HERE1; { my \$package = $package; eval "package \$package;" . <<'EOCODE'; use base qw(Workflow::Action); sub execute { my( \$self, \$wf ) = \@_; my \$sm = { }; my \%data = ( local => Gestinanna::XSM::Workflow::xsm_context_params(undef, \$wf), session => { }, ); $script_start $$action{code} } EOCODE } 1HERE1 $code .= <<1HERE1; $name => { class => $name, field => [ 1HERE1 foreach my $field (@{$action->{field}||[]}) { $name = Gestinanna::XSM::makeSingleQuoted($field->{name}); my $required = Gestinanna::XSM::makeSingleQuoted($field -> {is_required}); my $type = Gestinanna::XSM::makeSingleQuoted($field -> {type}); $code .= <<1HERE1; { name => $name, is_required => $required, param => [ 1HERE1 foreach my $param (@{$field -> {param}||[]}) { $name = Gestinanna::XSM::makeSingleQuoted($param->[0]); my $value = Gestinanna::XSM::makeSingleQuoted($param->[1]); $code .= <<1HERE1; [ $name, $value ], 1HERE1 } $code .= <<1HERE1; ], }, 1HERE1 } $code .= <<1HERE1 ], }, 1HERE1 } $code .= ");\n"; $code .= "\n%CONDITIONS = (\n"; foreach my $cond (@{$data -> {condition} || []}) { my $package = qq{\__PACKAGE__ . "::Conditions::$$cond{name}"}; #my $name = Gestinanna::XSM::makeSingleQuoted($cond -> {name}); my $name = $package; my $accessors = join("\n ", map { Gestinanna::XSM::makeSingleQuoted($_ -> [0]) } @{$cond->{params}||[]}); $pkg_code .= <<1HERE1; { my \$package = $package; eval "package \$package; " . <<'EOCODE'; use base qw(Workflow::Condition); \__PACKAGE__ -> mk_accessors( $accessors ); sub _init { my ( \$self, \$params ) = \@_; my \%data = ( local => \$params, self => \$self, session => { }, ); $script_start $$cond{init} } sub evaluate { my( \$self, \$wf ) = \@_; my \%data = ( local => Gestinanna::XSM::Workflow::xsm_context_params(undef, \$wf), session => { }, ); $script_start $$cond{code} } EOCODE } 1HERE1 #$package =~ s{^__PACKAGE__(.*)$}{__PACKAGE__ . "$1"}; $code .= <<1HERE1; $name => { class => $package, param => [ 1HERE1 foreach my $p (@{$cond -> {params}||[]}) { $name = Gestinanna::XSM::makeSingleQuoted($p -> [0]); my $value = Gestinanna::XSM::makeSingleQuoted($p -> [1]); $code .= <<1HERE1 $name => $value, 1HERE1 } $code .= <<1HERE1; ], }, 1HERE1 } $code .= ");\n"; return $code . $pkg_code; } elsif ($tag eq 'inherit') { } elsif( $tag eq 'state') { push @{$e->{WF_STATE}||=[]}, { name => $attribs{id}, action => $e->{WF_ACTIONS} || [], description => $e -> state('description'), }; delete $e -> {WF_ACTIONS}; $e -> pop_state; } elsif( $tag eq 'description') { my $d = $e -> state('text'); $d =~ s{\s+}{ }gm; $e -> pop_state; $e -> set_state('description', $d); } elsif( $tag eq 'action') { if($attribs{'id'}) { # defining the action with the enclosed script push @{$e -> {WF_DEF_ACTIONS}||=[]}, { name => $attribs{'id'}, code => $e -> state('script'), field => $e -> {WF_FIELDS} || [], validator => $e -> {WF_VALIDATORS} || [], }; $e -> pop_state; delete $e -> {WF_FIELDS}; delete $e -> {WF_VALIDATORS}; } elsif($attribs{'xref'}) { # referencing a definition from elsewhere if($attribs{'xref'} =~ m{^[A-Za-z_][A-Za-z_0-9]+$}) { push @{$e -> {WF_ACTIONS}||= []}, { name => $attribs{'xref'}, resulting_state => $attribs{'resulting-state'}, condition => $e -> {WF_CONDITIONS} || [], }; } else { warn "Illegal xref: [$attribs{'xref'}]\n"; } delete $e -> {WF_CONDITIONS}; } } elsif( $tag eq 'condition') { if($attribs{'id'}) { push @{$e -> {WF_DEF_CONDITIONS} ||= []}, { name => $attribs{'id'}, code => $e -> state('script'), init => $e -> state('init'), params => $e -> {WF_PARAMS} || [], }; $e -> pop_state; } } elsif( $tag eq 'init' ) { my $script = $e -> state('script'); $e -> pop_state; $e -> append_state('init', $script); } elsif( $tag eq 'validator') { if($attribs{'id'}) { push @{$e -> {WF_DEF_VALIDATORS} ||= []}, { name => $attribs{'id'}, code => $e -> state('script'), params => $e -> {WF_PARAMS} || [], }; $e -> pop_state; } } elsif( $tag eq 'param') { push @{$e -> {WF_PARAMS} ||= [ ]}, [ $attribs{id}, $attribs{value} ]; my $init = $e -> state('init'); if($init ne '') { $init = '$init}; } $e -> pop_state; $e -> append_state('init', $init); } elsif( $tag eq 'field') { push @{$e -> {WF_FIELDS}}, { name => $attribs{id}, is_required => $attribs{required} || 'no', type => $attribs{type}, source => $e -> state('script'), source_class => $attribs{'source-class'}, param => $e -> {WF_PARAMS} || [], }; $e -> pop_state; $e -> {WF_PARAMS} = [ ]; } elsif( $tag eq 'add-context' ) { my $script = $e -> state('script'); $e -> pop_state; $e -> set_state(script => $script); return '])' . $e -> semi; } elsif( $tag eq 'choose-action' ) { my $script = $e -> state('script'); $script = "" unless defined $script; my $o = $e -> state('choose-action-otherwise'); $e -> pop_state; if($e -> state('in-expression')) { if($o) { return "$script $o ) )[1])"; } return "$script () ) ) )"; } if(defined $o && $o ne '') { $script .= " } else { $o }"; } else { $script .= " }"; } $script =~ s{;\s*if\(0\)\s*{\s*}\s*els}{; }; $script =~ s{\s*else\s*{\s*}$}{}; return $script; } elsif( $tag eq 'when' ) { if($e -> state('in-expression')) { return ' ) : '; } return ""; } elsif( $tag eq 'otherwise' ) { my $script = $e -> state('script'); $e -> pop_state; $e -> set_state('choose-action-otherwise', $script); return ''; } elsif( $tag eq 'add-history' ) { my $script = $e -> state('script'); my $a = '%a' . $e -> state('in-association'); $e -> pop_state; $e -> set_state(script => $script); return "; \\$a; })" . $e -> semi; } return ''; } sub execute_action { my($wf, $action) = @_; return unless $wf; eval { $wf -> execute_action($action) }; if($@) { warn "$@\n"; return 0; } return 1; } sub add_history { my($wf, $attrs) = @_; return unless $wf; my %params; if(defined $attrs->{action}) { $params{action} = $attrs->{action}; } else { $params{action} = 'No action specified.'; } if(defined $attrs->{description}) { $params{description} = $attrs->{description}; } else { $params{description} = 'No description.'; } $params{state} = $wf -> state; $wf -> add_history( \%params ); } sub xsm_create($$) { my $type = $_[1]; my $factory = Gestinanna::Request -> instance -> config -> workflow_factory; return $factory -> create_workflow($type); } sub xsm_fetch($$$) { my $type = $_[1]; my $id = $_[2]; my $factory = Gestinanna::Request -> instance -> config -> workflow_factory; return $factory -> fetch_workflow($type, $id); } sub xsm_create_context($$) { my $list = $_[1]; use Data::Dumper; warn "xsm_create_context(...): ", Data::Dumper -> Dump([$list]); $list = [ $list ] unless UNIVERSAL::isa($list, 'ARRAY'); my $c = Workflow::Context -> new; _add_hash($c, '', $_) foreach @$list; warn "Resulting context: ", Data::Dumper -> Dump([$c]); return $c; } sub xsm_add_context($$$) { my $wf = $_[1]; my $list = $_[2]; my $c = xsm_create_context($_[0], $list); warn "Context returned\n"; $wf -> context -> param($c -> param()); warn "Context merged\n"; } sub _add_hash { my($c, $prefix, $hash) = @_; warn "_add_hash($c, $prefix, $hash)\n"; if(UNIVERSAL::isa($hash, 'ARRAY')) { for(my $i = 0; $i < @$hash; $i++) { if(ref $hash->[$i]) { _add_hash($c, "${prefix}${i}.", $hash->[$i]); } else { $c -> param("${prefix}${i}", $hash->[$i]); } } } elsif(UNIVERSAL::isa($hash, 'HASH')) { foreach my $k (keys %$hash) { if(ref $hash->{$k}) { _add_hash($c, "${prefix}${k}.", $hash -> {$k}); } else { $c -> param("${prefix}${k}", $hash->{$k}); } } } } sub xsm_context_params($$) { my $c = $_[1]; $c = $c -> [0] if UNIVERSAL::isa($c, 'ARRAY'); $c = $c -> context if UNIVERSAL::isa($c, "Workflow"); warn "xsm_context_params(..., $c)\n"; return { } unless $c; my $hash = $c -> param; my $ret = { }; foreach my $k (keys %$hash) { Gestinanna::XSM::Expression::set_element($ret, [split(/\./, $k)], $hash->{$k}); } use Data::Dumper; warn "context: ", Data::Dumper -> Dump([$c]); warn "returning ", Data::Dumper -> Dump([$ret]); return $ret; } sub xsm_find($$$;$) { my($sm, $type, $state, $user) = @_; # still need to do searches # $state may be a list of values my $factory = Gestinanna::Request -> instance -> config -> workflow_factory; my $pof_type = 'workflow'; my $pof_factory = Gestinanna::Request -> instance -> factory; my @where; push @where, 'AND' if $state || $user; if($state) { if(ref $state) { push @where, [ state => 'IN' => @$state ]; } elsif($state) { push @where, [ state => '=' => $state ]; } } if(ref $user) { push @where, [ user_type => '=' => $user -> [0] ]; push @where, [ user_id => '=' => $user -> [1] ]; } elsif($user) { push @where, [ user_type => '=' => 'actor']; push @where, [ user_id => '=' => $user ]; } use Data::Dumper; warn "Where: ", Data::Dumper -> Dump([\@where]); my $iterator = $pof_factory -> find( $pof_type => ( where => [ @where, [ type => '=' => $type ] ], ) ); my @requests; my $id; while($id = $iterator -> next_id) { warn "Found $id\n"; push @requests, $factory -> fetch_workflow($type, $id); } return @requests; } 1;