####
# Functions implementing sm:* processing
####
package Gestinanna::XSM::StateMachine;
our @ISA = qw(Gestinanna::XSM);
our $NS = 'http://ns.gestinanna.org/statemachine';
#__PACKAGE__ -> register;
sub start_document {
return "#initialize sm namespace\nuse Gestinanna::Request;\n";
}
sub end_document {
return '';
}
sub comment {
return '';
}
sub processing_instruction {
return '';
}
sub characters {
my ($e, $text) = @_;
if($e -> state('in-text')) {
return "\"\Q$text\E\"";
}
$e -> append_state('text', $text);
return '';
}
sub start_element {
my ($e, $node) = @_;
my ($tag, %attribs);
$tag = $node->{Name};
foreach my $attrib (@{$node->{Attributes}}) {
$attribs{$attrib->{Name}} = $attrib->{Value};
}
if ($tag eq 'statemachine') {
# need to store the package name
# go ahead and put the package here
if($attribs{inherit}) {
$e -> {SM_EDGES} -> {_INHERIT} = $e -> makeSingleQuoted($attribs{inherit});
}
return '';
}
elsif ($tag eq 'inherit') {
#warn qq{\n};
my $class;
if($attribs{class}) {
eval "require \Q$attribs{class}\E;";
if($@) {
warn "Unable to load $attribs{class}\n";
}
else {
$class = $attribs{class};
}
}
elsif($attribs{name}) {
# compile file and push resulting package into SM_HASA
my @path = map { s{/[^/]+$}{/}; $_ } (
$e -> {filename}
); # basically, current package filename
foreach my $p (@path) {
$class = $e->{compiler} -> (File::Spec::Unix -> rel2abs($attribs{name}, $p));
last unless ref $class;
}
if(ref $class) {
warn "Unable to compile $attribs{name}\n";
return '';
}
#$class = $attribs{name}; # for later
}
#warn "Compiled $attribs{name} into $class\n";
return '' unless defined $class;
if($attribs{id}) {
$e -> {SM_HASA} -> {$attribs{id}} = $class;
}
else {
push @{$e -> {SM_ISA}||=[]}, $class;
}
if($class =~ m{::(v\d+(_\d+)*)$}) {
#push @{$e -> {SM_FILES}||=[]}, $attribs{name} . "/$1";
push @{$e -> {SM_FILES}||=[]}, join('/', $class -> filename, $1);
}
return '';
}
elsif ($tag eq 'alias') {
return '' if $e -> state('in-script');
$e -> {SM_ALIASES} -> {$e -> makeSingleQuoted($attribs{id})} = $e -> makeSingleQuoted($attribs{state});
return '';
}
elsif ($tag eq 'state') {
if($attribs{id} !~ m{^[a-z][0-9a-z_]+}) {
warn "\@id ($attribs{id}) for state element does not match m{^[a-z][0-9a-z_]+}";
return '';
}
$e -> set_state('state-id', $attribs{id}) if defined $attribs{id};
#warn "Entering state: $attribs{id}\n";
defined $attribs{id} or warn("No \@id for state element");
$e -> {SM_EDGES}{$attribs{id}} ||= { };
$e -> {SM_EDGES}{$attribs{id}}{_INHERIT} = $e -> makeSingleQuoted($attribs{inherit}) if defined $attribs{inherit};
$e -> {SM_VIEWS}{$attribs{id}} = $e -> makeSingleQuoted($attribs{view}) if defined $attribs{view};
$e -> {SM_ERROR}{$attribs{id}}{prefix} = $e -> makeSingleQuoted($attribs{'error-prefix'}) if defined $attribs{'error-prefix'};
$e -> {SM_ERROR}{$attribs{id}}{format} = $e -> makeSingleQuoted($attribs{'error-format'}) if defined $attribs{'error-format'};
return '';
}
elsif ($tag eq 'transition') {
if($attribs{state} !~ m{^[_a-z][0-9a-z_]+}) {
warn "\@state ($attribs{state}) for transition element does not match m{^[_a-z][0-9a-z_]+}";
return '';
}
$e -> set_state('transition-id', $attribs{state}) if defined $attribs{state};
defined $attribs{state} or warn("No \@state for transition element");
$e -> {SM_EDGES}{$e -> state('state-id')}{$attribs{state}} ||= { };
$e -> {SM_EDGES}{$e -> state('state-id')}{$attribs{state}}{_INHERIT}
= $e -> makeSingleQuoted($attribs{inherit}) if defined $attribs{inherit};
return '';
}
elsif ($tag eq 'variable') {
# we're in a spec guarding a transition
my $state = $e -> state('state-id');
my $trans = $e -> state('transition-id');
my $group = $e -> state('group-id');
$state = '_' unless defined $state;
$trans = '_' unless defined $trans;
my $id = $e -> makeSingleQuoted($attribs{'id'});
if(defined $group) {
$id = $e -> makeSingleQuoted(eval "$group . '.' . $id")
}
else {
$group = '_' unless defined $group;
}
$e -> set_state('variable-id', $id);
my $var_info = {
id => $id,
};
if(defined $attribs{'dependence'}) {
$var_info->{'dependence'} = $e -> makeSingleQuoted($attribs{'dependence'});
}
elsif(defined $e -> {SM_VARS}{$state}{$trans}{$group}{'_'}{dependence}) {
$var_info->{'dependence'} = $e -> {SM_VARS}{$state}{$trans}{$group}{'_'}{dependence};
}
$e -> {SM_VARS} -> {$state}{$trans}{$group}{$id} = $var_info;
return '';
}
elsif ($tag eq 'constraint') {
$e -> reset_state('params');
return '';
}
elsif ($tag eq 'filter') {
my $id = $attribs{id};
my $state = $e -> state('state-id');
my $trans = $e -> state('transition-id');
my $group = $e -> state('group-id');
my $var = $e -> state('variable-id');
$state = '_' unless defined $state;
$trans = '_' unless defined $trans;
$group = '_' unless defined $group;
$var = '_' unless defined $var;
my $code = '';
if($id =~ m{:}) {
my $ns;
($ns, $id) = split(/:/, $id, 2);
# now we need to translate $ns to a namespaceuri
my $tns = $e -> {Current_Element}{Namespaces}{$ns};
warn "Unknown namespace ($ns)" unless defined $tns;
$id =~ tr/-/_/;
$id =~ s{[^a-zA-Z_0-9]+}{}g;
$id = "filter_$id";
my $pkg = $e -> ns_handler($tns);
$code .= "\\\&${pkg}::${id}";
}
else { # default ns for filters is Data::FormValidator's own packaged filters
$code = $e -> makeSingleQuoted($id);
}
if($code) {
#warn "Pushing [$code] onto filters stack for <$state><$trans><$group><$var>\n";
push @{$e -> {SM_VARS}{$state}{$trans}{$group}{$var}{filters}||=[]}, $code;
}
return '';
}
elsif ($tag eq 'group') {
return '' if $e -> state('in-script');
my $state = $e -> state('state-id');
my $trans = $e -> state('transition-id');
$state = '_' unless defined $state;
$trans = '_' unless defined $trans;
my $id;
my %info;
if($id = $e -> state('group-id')) {
$info{dependence} = $e -> {SM_VARS}{$state}{$trans}{$id}{'_'}{dependence};
$id = (eval $id) . "." . $attribs{'id'};
$id = $e -> makeSingleQuoted($id);
}
else {
$id = $e -> makeSingleQuoted($attribs{'id'});
}
$e -> set_state('group-id', $id);
for my $a (qw(some dependence)) {
$info{$a} = $e -> makeSingleQuoted($attribs{$a})
if defined $attribs{$a} && $attribs{$a} ne '';
}
#warn "\$e -> {SM_VARS}{$state}{$trans}{$id}{'_'}: ", Data::Dumper -> Dump([\%info]);
$e -> {SM_VARS}{$state}{$trans}{$id}{'_'} = \%info;
return '';
}
elsif ($tag eq 'script') {
my $sub_name;
if($sub_name = $e -> state('state-id')) {
if($e -> state('transition-id')) {
$sub_name .= "_to_" . $e -> state('transition-id');
}
else {
if($attribs{when} eq 'post' || $attribs{when} eq 'pre') {
$sub_name = $attribs{when} . "_" . $sub_name;
}
elsif($attribs{when}) {
warn "Unrecognized value for \@when for script element ('$attribs{when}' should be 'pre' or 'post')";
}
}
} else {
if($attribs{when} eq 'pre') {
$sub_name = 'initialize';
}
elsif($attribs{when} eq 'post') {
$sub_name = 'cleanup';
}
elsif($attribs{when}) {
warn "Unrecognized value for \@when for script element ('$attribs{when}' should be 'pre' or 'post')";
}
}
$e -> enter_state('in-script');
$e -> push_state;
$e -> reset_state('in-expression');
$e -> set_state('script-name', $sub_name);
my $ret = "sub $sub_name { \n" . <<'1HERE1'; # . "warn \"Entering $sub_name\n\";";
my($sm) = shift;
my %vars;
my $R = Gestinanna::Request -> instance;
my %data = (
local => ($sm -> data -> {'out'} ||= {}),
context => $sm -> data,
solar => { },
global => { },
);
local($_) = local($topic) = $data{local};
1HERE1
# need to go through each namespace and get the start of script stuff...
$ret .= $e -> get_script_start;
#warn "Start script: ", $e -> get_script_start, "\n";
#$ret .= <<1HERE1;
#warn "Entering " . __PACKAGE__ . "::$sub_name\n";
#1HERE1
# check to see if script can access the session data or not
$ret .= < SUPER::$sub_name;
return \$state if defined \$state;
}
EOF
return $ret;
}
elsif($tag eq 'goto') {
if($attribs{'state-machine'}) {
my $sm = $e -> static_expr($attribs{'state-machine'});
my $code = "Gestinanna::XSM::Op -> goto(filename => ($sm)[0]";
if($attribs{state}) {
my $state = $e -> static_expr($attribs{state});
$code .= ", state => ($state)[0]";
}
if($attribs{'next-state'}) {
my $state = $e -> static_expr($attribs{'next-state'});
$code .= ", 'next-state' => ($state)[0]";
}
elsif($e -> state('state-id')) {
$code .= ", 'next-state' => " . $e -> makeSingleQuoted($e -> state('state-id'));
}
return $code . ", args => " . $e -> enter_param;
}
if($attribs{state}) {
my $state = $e -> static_expr($attribs{state});
return "return ( ($state)[0] )" . $e -> semi;
}
}
elsif ($tag eq 'assert') {
my $state = $attribs{state};
$e -> push_state;
#$e -> set_state('assert-state', $attribs{state});
if($e -> state('in-expression')) {
return "(" . Gestinanna::XSM::compile_expr($e, $attribs{test}) . ") ?\n";
}
else {
return "unless(" . Gestinanna::XSM::compile_expr($e, $attribs{test}) . ") {\n";
}
return "return \"\Q$state\E\" unless(" . Gestinanna::XSM::compile_expr($e, $attribs{test}) . ");\n";
}
else {
warn("Unrecognised tag: $tag");
}
return '';
}
sub end_element {
my ($e, $node) = @_;
my($tag, %attribs);
$tag = $node->{Name};
foreach my $attrib (@{$node->{Attributes}}) {
$attribs{$attrib->{Name}} = $attrib->{Value};
}
if ($tag eq 'statemachine') {
# need to get everything together for the tail of the package
my $ret = '';
my %seen;
# handle inheritances
if(grep {defined} @{$e -> {SM_ISA}||[]}) {
$ret .= "our \@ISA = (" . join(", ", map { $e -> makeSingleQuoted($_) } grep { !$seen{$_}++ } grep { defined } @{$e -> {SM_ISA}||[]}) . ");";
}
else {
$ret .= "our \@ISA = ('Gestinanna::XSM::Base');";
}
%seen = ( );
if(@{$e -> {SM_FILES}||[]}) {
$ret .= "our \@FILES = (" . join(", ", map { $e -> makeSingleQuoted($_) } grep { !$seen{$_}++ } @{$e -> {SM_FILES}||[]}) . ");";
}
if(keys %{$e -> {SM_HASA}||{}}) {
$ret .= "our \%HASA = ("
. join( ", ",
map { $e -> makeSingleQuoted($_) => $e -> makeSingleQuoted($e -> {SM_HASA} -> {$_}) }
keys %{$e -> {SM_HASA}}
)
. "); ";
}
if(keys %{$e -> {SM_VIEWS}||{}}) {
$ret .= "our \%VIEWS = ("
. join( ", ",
map { $e -> makeSingleQuoted($_) => $e -> {SM_VIEWS} -> {$_} }
keys %{$e -> {SM_VIEWS}}
)
. "); ";
}
if(keys %{$e -> {SM_ALIASES}||{}}) {
$ret .= "our \%ALIASES = ("
. join( ", ",
map { $_ => $e -> {SM_ALIASES} -> {$_} }
keys %{$e -> {SM_ALIASES}}
)
. "); ";
}
# handle various data tables? or build them piecemeal in the code?
# at least do aliases here
my $edges_code = 'our %EDGES = ( ';
$edges_code .= "_INHERIT => " . $e -> {SM_EDGES}{_INHERIT} . ", "
if $e -> {SM_EDGES}{_INHERIT};
foreach my $state (keys %{$e -> {SM_EDGES} || {}}) {
#warn "Looking at state $state\n";
next if $state eq '_INHERIT';
$edges_code .= $e -> makeSingleQuoted($state) . ' => {';
$edges_code .= "_INHERIT => " . $e -> {SM_EDGES}{$state}{_INHERIT} . ","
if $e -> {SM_EDGES}{$state}{_INHERIT};
foreach my $trans (keys %{$e -> {SM_EDGES}{$state} || {}}) {
#warn " Looking at transition $trans\n";
next if $trans eq '_INHERIT';
$edges_code .= $e -> makeSingleQuoted($trans) . ' => {';
$edges_code .= "_INHERIT => " . $e -> {SM_EDGES}{$state}{$trans}{_INHERIT} . ","
if $e -> {SM_EDGES}{$state}{$trans}{_INHERIT};
my $info = {
optional => [ ],
required => [ ],
};
my %vars;
my %some;
my %some_deps;
foreach my $vars (
$e -> {SM_VARS}{'_'}{'_'},
$e -> {SM_VARS}{$state}{'_'},
$e -> {SM_VARS}{$state}{$trans},
) {
next unless defined $vars;
foreach my $g (keys %{$vars}) {
#warn "Looking at group $g\n";
#warn "Group: " . Data::Dumper -> Dump([$vars -> {$g}]);
foreach my $v (keys %{$vars -> {$g}||{}}) {
next if $v eq '_';
my $id = $v;
#warn " Looking at var $id\n";
if(exists $vars{$id}) {
foreach my $k (keys %{$vars -> {$g}{$v}||{}}) {
if($k eq 'dependence') {
$vars{$id}{$k} = $vars -> {$g}{$v}{$k};
}
elsif($k =~ m{^filters|constraints$}) {
push @{$vars{$id}{$k}||=[]}, @{$vars -> {$g}{$v}{$k}||[]};
push @{$vars{$id}{$k}||=[]}, @{$vars -> {$g}{'_'}{$k}||[]};
}
}
$vars{$id}{'dependence'} = $vars -> {$g}{'_'}{'dependence'} unless defined $vars{$id}{'dependence'};
}
else {
$vars{$id} = { %{$vars -> {$g}{$v}||{}} };
$vars{$id} -> {id} = $id;
$vars{$id}{'dependence'} = $vars -> {$g}{'_'}{'dependence'} unless defined $vars{$id}{'dependence'};
}
}
my $s;
if(($s = $vars->{$g}{'_'}{'some'}) && defined $s && $s ne 'q||') {
#warn "Got $s for $g\n";
$some{$g} = "[ $s, " . join(', ', grep { $_ ne '_' } keys %{$vars->{$g}}) . "]";
@some_deps{keys %{$vars->{$g}}} = undef;
}
#warn "some for $g: $some{$g}\n";
#warn "some_deps: ", join(", ", keys %some_deps), "\n";
# need to do group-based constraints here
}
}
use Data::Dumper;
#warn "Vars: " , Data::Dumper -> Dump([\%vars]);
my @global_filters;
foreach my $vars (
$e -> {SM_VARS}{'_'}{'_'}{'_'}{'_'},
$e -> {SM_VARS}{$state}{'_'}{'_'}{'_'},
$e -> {SM_VARS}{$state}{$trans}{'_'}{'_'},
) {
foreach my $v (keys %vars) {
push @{$vars{$v}{constraints}||=[]}, @{$vars -> {'constraints'}||[]};
}
push @global_filters, @{$vars -> {'filters'}||[]};
}
$edges_code .= "optional => [" . join(", ", grep { !exists $some_deps{$_} && defined $vars{$_} -> {dependence} && $vars{$_} -> {dependence} eq 'q|OPTIONAL|' } keys %vars ) . "], ";
$edges_code .= "required => [" . join(", ", grep { !exists $some_deps{$_} && !defined $vars{$_} -> {dependence} || $vars{$_} -> {dependence} eq 'q||' } keys %vars ) . "], ";
$edges_code .= 'constraints => { ';
foreach my $v (keys %vars) {
next unless @{$vars{$v}{constraints}||[]};
next if $v =~ m{\*$}; # wildcard ending
$edges_code .= " $v => [ " . join(", ", grep { defined && $_ ne 'q||' } @{$vars{$v}{constraints}}) . "], ";
}
$edges_code .= '}, field_filters => { ';
foreach my $v (keys %vars) {
next unless @{$vars{$v}{filters}||[]};
next if $v =~ m{\*$}; # wildcard ending
$edges_code .= " $v => [ " . join(", ", @{$vars{$v}{filters}}) . "], ";
}
$edges_code .= '}, filters => [ ' . join(", ", @global_filters) . ' ], ';
# still need dependency_groups, dependencies defaults overrides
#
my @dependencies = keys %{ +{ map { $vars{$_}->{dependence} => undef } grep { defined($vars{$_}->{dependence}) && $vars{$_}->{dependence} ne 'q|OPTIONAL|' } keys %vars } };
if(@dependencies) {
$edges_code .= 'dependencies => {';
foreach my $d (@dependencies) {
next if $d eq 'q||';
$edges_code .= "$d => [ "
. join(", ", grep { $vars{$_} -> {dependence} eq $d } keys %vars)
. '], ';
}
$edges_code .= '}, ';
}
#warn Data::Dumper -> Dump([\%some], [qw(*some)]);
if(keys %some) {
$edges_code .= 'require_some => {'
. join(", ", map { join(' => ', $_, $some{$_}) } keys %some)
. '}, ';
}
$edges_code .= '}, ';
}
$edges_code .= '}, ';
}
$edges_code .= ');';
$ret .= $edges_code;
#warn "Edges code: $edges_code\n";
return $ret;
}
elsif ($tag eq 'state') {
$e -> reset_state('state-id');
}
elsif ($tag eq 'transition') {
$e -> reset_state('transition-id');
}
elsif ($tag eq 'variable') {
$e -> reset_state('variable-id');
return '';
}
elsif ($tag eq 'constraint') {
my $state = $e -> state('state-id');
my $trans = $e -> state('transition-id');
my $group = $e -> state('group-id');
my $var = $e -> state('variable-id');
$state = '_' unless defined $state;
$trans = '_' unless defined $trans;
$group = '_' unless defined $group;
$var = '_' unless defined $var;
my $params = $e -> state('params');
#warn "my params: $params\n";
my @code;
my $id = $attribs{id};
my $constraint;
if($attribs{equal}) {
push @code, "sub { \$_[0] eq " . $e -> makeSingleQuoted($attribs{equal}) . " }";
}
if($attribs{'max-length'}) {
push @code, "sub { length(\$_[0]) <= " . $e -> makeSingleQuoted($attribs{'max-length'}) . " }";
}
if($attribs{'min-length'}) {
push @code, "sub { length(\$_[0]) >= " . $e -> makeSingleQuoted($attribs{'min-length'}) . " }";
}
if($attribs{'length'}) {
push @code, "sub { length(\$_[0]) == " . $e -> makeSingleQuoted($attribs{'length'}) . " }";
}
unless(defined $id) {
}
elsif($id =~ m{:}) {
my $ns;
($ns, $id) = split(/:/, $id, 2);
# now we need to translate $ns to a namespaceuri
my $tns = $e -> {Current_Element}{Namespaces}{$ns};
warn "Unknown namespace ($ns)" unless defined $tns;
$id =~ tr/-/_/;
$id =~ s{[^a-zA-Z_0-9]+}{}g;
$id = "valid_$id";
my $pkg = $e -> ns_handler($tns);
$constraint = "\\\&${pkg}::${id}";
}
else { # default ns for constraints is Data::FormValidator's own packaged constraints
if($id eq 'equal') {
$constraint = 'sub { $_[0] eq $_[1] }';
}
else {
$constraint = $e -> makeSingleQuoted($id);
}
}
if($constraint && $params) {
if($var ne '_' && $params !~ m{(^|,\s*)\Q$var\E(,|$)}) {
$params = $var . ", $params";
}
push @code, < $constraint,
params => [ $params ],
}
EOF
}
else {
push @code, $constraint,
}
if($attribs{'max-length'}) {
push @code, "sub { length(\$_[0]) <= " . $e -> makeSingleQuoted($attribs{'max-length'}) . " }";
}
if($attribs{'min-length'}) {
push @code, "sub { length(\$_[0]) >= " . $e -> makeSingleQuoted($attribs{'min-length'}) . " }";
}
if(@code) {
#warn "Pushing [" . join(";;;", @code) . "] onto constraints stack for <$state><$trans><$group><$var>\n";
push @{$e -> {SM_VARS}{$state}{$trans}{$group}{$var}{constraints}||=[]}, @code;
}
return '';
}
elsif ($tag eq 'filter') {
return '';
}
elsif ($tag eq 'group') {
return '' if $e -> state('in-script');
my $id = $e -> state('group-id');
$id = eval $id;
my $own_id = $attribs{'id'};
$id =~ s{\.?\b\Q$own_id\E$}{};
if($id ne '') {
$e -> set_state('group-id', $e -> makeSingleQuoted($id));
}
else {
$e -> reset_state('group-id');
}
}
elsif ($tag eq 'script') {
my $script = $e -> state('script');
my $script_super = $attribs{'super'};
my $sub_name = $e -> state('script-name');
$e -> pop_state;
$e -> set_state('script', $script);
$e -> leave_state('in-script');
my $ret = "";
$ret .= $e -> get_script_end;
return "$ret\n}" . $e -> semi if $e -> state('in-script');
return < SUPER::$sub_name;
return \$state if defined \$state;
}
return;
}
EOF
return " return;\n}\n";
}
elsif ($tag eq 'assert') {
my $state = $e -> static_expr($attribs{state});
my $script = $e -> state('script');
$e -> pop_state;
$e -> set_state('script', $script);
return "return $state; }" . $e -> semi;
}
elsif($tag eq 'goto') {
if($attribs{'state-machine'}) {
return $e -> leave_param . $e -> semi;
my $script = $e -> state('script');
my $a = '%goto' . $e -> state('in-goto');
$e -> pop_state;
$e -> set_state(script => $script);
return "; \\$a; } )" . $e -> semi;
}
return '';
}
return '';
}
sub path_to_dotted { my $p = $_[0]; $p =~ tr[/][.]; return $p; }
1;
__END__