package StateML::SAXHandler ; $VERSION = 0.001 ; =head1 NAME StateML::SAXHandler - convert a SAX stream to a StateML::Machine =head1 SYNOPSIS ## From StateML::parse() require StateML::SAXHandler; require XML::Filter::Mode; my $handler = StateML::SAXHandler->new( $machine ) ; my $mode_filter = XML::Filter::Mode->new( Handler => $handler, Modes => [$machine->modes], ); ## Require PurePerl for now to get the bugs out local $XML::ParserPackage = "XML::SAX::PurePerl"; my $p = XML::SAX::ParserFactory->parser( Handler => $mode_filter, UseAttributeOrder => 1, Source => $source, ) ; return $p->parse ; =head1 DESCRIPTION Use like a normal SAX handler, then collect the machine from the SAX pipline's end_document() event. See L for an oft-used prefilter to "shape" your machines by giving elements to nodes. See L::parse() for source examples. =cut use strict ; use Carp ; use StateML::Machine; use StateML::Constants qw( stateml_1_0_ns ); sub new { my $proto = shift ; my $class = ref $proto || $proto ; my $self = bless { }, $class ; $self->machine( @_ ) if @_ ; return $self ; } sub machine { my $self = shift ; $self->{MACHINE} = shift if @_ ; return $self->{MACHINE} ; } sub parser { my $self = shift ; $self->{PARSER} = shift if @_ ; return $self->{PARSER} ; } sub _location { my $self = shift ; return '';# unless $self->{DOC_LOCATOR}; my $pl = $self->{DOC_LOCATOR}; return join( " ", "", "at", defined $pl->{SystemId} ? "$pl->{SystemId}," : (), "line", $pl->{LineNumber}, "column", $pl->{ColumnNumber} ) ; } sub _X { my $self = shift ; die @_, $self->_location, "\n" ; } sub _top { return shift->{ELT_STACK}->[-1] ; } sub set_document_locator { my $self = shift; $self->{DOC_LOCATOR} = shift; } sub start_document { my $self = shift ; $self->{ELT_STACK} = [ { ELT => {Name => "#root"}}, ## Fake elt, so stack never empty ] ; $self->{InStateMLElt} = [ 0 ]; } sub end_document { shift->machine } ## Poor excuse for a DTD :) my %can_be_in = ( "machine" => [ "#root" ], "class" => [ qw( machine state ) ], "state" => [ qw( machine state ) ], "event" => [ qw( machine arc ) ], "action" => [ qw( machine ) ], "name" => [ qw( state event action ) ], "description" => [ qw( machine event state arc action ) ], "version" => [ qw( machine ) ], "preamble" => [ qw( machine ) ], "postamble" => [ qw( machine ) ], "api" => [ qw( event ) ], "pre-handler" => [ qw( event ) ], "post-handler" => [ qw( event ) ], "entry-handler" => [ qw( state ) ], "exit-handler" => [ qw( state ) ], "arc" => [ qw( state ) ], "handler" => [ qw( event arc action ) ], ) ; sub _stateml_attrs { my ( $elt ) = @_; ## Return a list of ( Name => $value ) pairs of all attrs in ## the empty namespace or in the StateML namespace ( map { ( my $key = uc $_->{LocalName} ) =~ s/-/_/g; my $value = $_->{Value}; $value = [ grep length, split /,/, $value ] if $key eq "CLASS_IDS"; ( $key => $value ); } grep $_->{NamespaceURI} eq "" || $_->{NamespaceURI} eq stateml_1_0_ns, values %{ $elt->{Attributes} } ); } sub start_element { my $self = shift ; my ( $elt ) = @_ ; push @{$self->{InStateMLElt}}, $elt->{NamespaceURI} eq stateml_1_0_ns; if ( $self->{InStateMLElt}->[-1] ) { push @{$self->{InStateMLElt}}, 1; my $parent = $self->_top ; my $parent_type = $parent->{ELT}->{Name} ; my $elt_type = $elt->{Name} ; croak "<$elt> may not have a missing prefix\n" if substr( $elt, 0, 1 ) eq ":" ; croak "<$elt> may not have multiple prefixes\n" if substr( $elt, 0, 1 ) eq ":" ; my $h = { ELT => $elt, LOCATION => $self->_location, _stateml_attrs $elt, } ; $elt_type =~ s/.*:// ; $elt->{Name} =~ s/.*:// ; $self->_X( "<$elt_type> not allowed in <$parent_type>" ) unless grep $parent_type eq $_, @{$can_be_in{$elt_type}} ; if ( $elt_type eq "api" ) { push @{$parent->{APIS}}, $h ; } elsif ( $elt_type =~ /amble$/ ) { $h->{CODE} = "" ; } elsif ( $elt_type =~ /handler$/ ) { $h->{CODE} = "" ; } elsif ( $elt_type eq "action" ) { $h = StateML::Action->new( %$h ) ; $self->machine->add( $h ) ; } elsif ( $elt_type eq "class" ) { $h = StateML::Class->new( %$h ) ; $self->machine->add( $h ) ; } elsif ( $elt_type eq "event" ) { $h = StateML::Event->new( %$h ) ; $self->machine->add( $h ) ; if ( UNIVERSAL::isa( $parent, "StateML::Arc" ) ) { ## Bypass event_id() to prevent defaulting my $old_event_id = $parent->{EVENT_ID}; my $id = $h->id; die " conflicts with \n" if defined $old_event_id && $old_event_id ne $id; $parent->event_id( $id ); } } elsif ( $elt_type eq "machine" ) { $self->{MACHINE_ID} = $h->{ID}; my $m = $self->machine ; $m->{$_} = $h->{$_} for grep !defined $m->{$_}, keys %$h; $h = $m ; $h->{ID} = "#MACHINE 1" unless defined $h->{ID}; } elsif ( $elt_type eq "state" ) { my $id = $h->{ID}; $self->_X( " missing an id=" ) unless defined $id ; my $old_h = $self->machine->state_by_id( $id ) ; if ( $old_h ) { $old_h->{LOCATION} .= " and $h->{LOCATION}" ; $h = $old_h ; $h->{ELT} = $elt ; } else { $h = StateML::State->new( %$h, ELT => $elt, ID => $id, PARENT_ID => $self->{MACHINE_ID}, ORDER => scalar $self->machine->states, ) ; $self->machine->add( $h ) ; } } elsif ( $elt_type eq "arc" ) { $h->{TO} = delete $h->{GOTO} if exists $h->{GOTO}; $h = StateML::Arc->new( %$h ) ; $h->from( $parent->id ) unless defined $h->from; $h->to( $parent->id ) unless defined $h->to; $self->machine->add( $h ) ; } $h->{LOCATION} = $self->_location ; $h->{ATTRS} = { map { ( $_ => $elt->{Attributes}->{Value} ); } keys %{$elt->{Attributes}} }; push @{$self->{ELT_STACK}}, $h ; } } sub characters { my $self = shift ; my ( $h ) = @_ ; return unless $self->{InStateMLElt}->[-1]; my $parent = $self->_top ; my $elt_type = $parent->{ELT}->{Name} ; if ( $elt_type =~ /handler$/ && ! $parent->{ACTION_ID} ) { $parent->{CODE} .= $h->{Data} ; } elsif ( $elt_type =~ /amble$/ ) { $parent->{CODE} .= $h->{Data} ; } elsif ( $elt_type eq "api" ) { $self->_X( "stack underflow" ) unless @{$self->{ELT_STACK}} >= 2 ; $self->{ELT_STACK}->[-2]->{API} .= $h->{Data} ; } elsif ( $elt_type eq "name" ) { $self->_X( "stack underflow" ) unless @{$self->{ELT_STACK}} >= 2 ; $self->{ELT_STACK}->[-2]->{NAME} .= $h->{Data} ; } elsif ( $elt_type eq "description" ) { $self->_X( "stack underflow" ) unless @{$self->{ELT_STACK}} >= 2 ; $self->{ELT_STACK}->[-2]->{DESCRIPTION} .= $h->{Data} ; } elsif ( $h->{Data} =~ /\S/ ) { my $location = $self->_location || '' ; warn "Ignoring '$h->{Data}' in <$elt_type>$location.\n" ; } } sub _deindent { my $ltrim; for ( $_[0] =~ /\n( *)\S/mg ) { $ltrim = length if ! defined $ltrim || length $_ < $ltrim; } $_[0] =~ s/[ \t]+$//; ## Remove trailing spaces. $_[0] =~ s/^([ \t]*\n)+//; ## Remove leading blank lines return unless defined $ltrim; $_[0] =~ s/^ {$ltrim}//mg; } sub end_element { my $self = shift ; my ( $elt ) = @_ ; if ( $self->{InStateMLElt}->[-1] ) { my $popped = pop @{$self->{ELT_STACK}} ; my $elt_type = $popped->{ELT}->{Name} ; $elt->{Name} =~ s/.*:// ; $self->_X( "{Name}> found, expected " ) unless $elt->{Name} eq $elt_type ; my $parent = $self->_top ; if ( $elt_type =~ /handler$/ ) { my $htype = uc $elt->{Name} ; $htype =~ tr/-/_/ ; my $code = defined $popped->{ACTION_ID} ? \$popped->{ACTION_ID} : do { _deindent $popped->{CODE} unless defined $parent->{DEINDENT} && ! $parent->{DEINDENT}; $popped->{CODE}; }; push @{$parent->{"${htype}S"}}, $code; } if ( $elt_type =~ /amble$/ ) { my $htype = uc $elt->{Name} ; _deindent $popped->{CODE} unless defined $parent->{DEINDENT} && ! $parent->{DEINDENT}; push @{$parent->{$htype}}, $popped->{CODE} ; } } pop @{$self->{InStateMLElt}}; } =head1 LIMITATIONS Alpha code. Ok test suite, but we may need to change things in non-backward compatible ways. =head1 COPYRIGHT Copyright 2003, R. Barrie Slaymaker, Jr., All Rights Reserved =head1 LICENSE You may use this module under the terms of the BSD, Artistic, or GPL licenses, any version. =head1 AUTHOR Barrie Slaymaker =cut 1 ;