=head1 NAME DBIx::Perform::DigestPer - "Perform" screen file digester Digests an Informixoid .per file and make a string suitable for writing to a file or just eval'ing. =head1 MODULE VERSION 0.04 =head1 SYNOPSIS use DBIx::Perform::DigestPer; $desc = digest(*INFILE_HANDLE); # now do the right thing with $desc shell> perl -MDBIx::Perform::DigestPer -e'digest_file("foo.per")' # writes file foo.pps or named in 2nd argument. # now read and do the right thing with foo.pps =head1 REQUIREMENTS Data::Dumper =head1 DESCRIPTION Digests an Informix "Perform" screen descriptor file into a form usable by the Perform emulator B. May be used inline or to write a file. Among other things, it digests the screen layout into a series of Curses widget specs, as either Label or TextField types. The output string/file is evaluable Perl source code, which sets four variables: $db: name of database $screen: screen descriptor, a hash including a Curses::Forms spec. Form fields' widgets are named as labelled (e.g. 'f000'). $tables: array of table names. $attrs: hash of field names to [table column attributes] . The 'attributes' string is unparsed. =cut package DBIx::Perform::DigestPer; use strict; use base 'Exporter'; use vars qw(@EXPORT_OK $VERSION %HEADING_WORDS); BEGIN { @EXPORT_OK = qw(digest digest_file); $VERSION = '0.04'; %HEADING_WORDS = map { ($_, 1) } qw(screen tables attributes instructions end); } use Data::Dumper; =head2 digest digest (IOHandle_Ref) Digests an Informix .per file into a string that evaluates to a Perform descriptor. =cut our $TABLES; sub digest # { shift if ($_[0] eq __PACKAGE__); my $ioh = shift; my $parser = new DBIx::Perform::DigestPer::Parser($ioh); my $word; my ($db, $tables, $atts, $instrs); my $screens = []; local $TABLES; # for attributes parser to check while ($word = $parser->readword('true')) { if ($word eq 'database') { $db = read_database($parser); } elsif ($word eq 'screen') { push (@$screens, read_screen($parser)); # might return many } elsif ($word eq 'tables') { $TABLES = $tables = read_tables($parser); } elsif ($word eq 'attributes') { $atts = read_attributes($parser); } elsif ($word eq 'instructions') { $instrs = read_instructions($parser); } } return outputstring($db, $screens, $tables, $atts, $instrs); } sub read_database { my $parser = shift; return $parser->readword(); # just the name. } sub read_screen { my @screens; my $parser = shift; my $result = {}; my $word; while ($word = $parser->readword()) { if ($word eq 'size') { # read size... my $height = 0 + $parser->readword(); my $by = $parser->readword(); my $width = 0 + $parser->readword(); die "Expected 'by' but got '$by'" if ($by ne 'by'); $result->{'MINSIZE'} = [ $width, $height]; } elsif ($word eq '{' ) { # read screen format my $widgets = {}; my $line; my $lineno = 0; my $labelno = '000'; my @fields = (); my $last_line_blank; my $page_split; while (defined($line = $parser->readline()) && $line !~ /\}/) { if ($line =~ /^\s*$/){ $page_split = 1 if $last_line_blank; $last_line_blank = 1; $lineno++; next; } while ($line =~ /(\[\s*(\w+)\s*\]|([^\s\[]+\s?)+)/g) { my $pre = $`; my $post = $'; #' my $match = $1; my $id = $2; if ($page_split) { push (@screens, { WIDGETS => $widgets, FIELDS => [ @fields ], LINES => $lineno, }); $widgets = {}; @fields = (); $lineno = 0; } undef $page_split; undef $last_line_blank; my $x = length($pre); # + $pos if ($id) { # it\'s a field my $cols = length($match) - 2; if (0) { # Leading bracket $widgets->{"${id}_openbracket"} = { TYPE => 'Label', COLUMNS => 1, Y => $lineno, X => $x, VALUE => '[' } ; } # The field... $x++; # Note, the OnEnter/OnExit subs must be supplied # by the Perform emulator. $widgets->{$id} = { TYPE => 'TextField', COLUMNS => $cols, Y => $lineno, X => $x, BORDER => 0}; $x += $cols; if (0) { # Trailing Bracket... $widgets->{"${id}_closebracket"} = { TYPE => 'Label', COLUMNS => 1, Y => $lineno, X => $x, VALUE => ']' }; } push (@fields, $id); } else { # it\'s a label $match =~ s/\s$//; # ignore trailing whitespace my $cols = length($match); $widgets->{"label_$labelno"} = { TYPE => 'Label', COLUMNS => $cols, Y => $lineno, X => $x, VALUE => $match }; $labelno++; } } $lineno++; } push (@screens, { WIDGETS => $widgets, FIELDS => [ @fields ], LINES => $lineno} ); } elsif (lc($word) eq 'end') { return @screens; } else { die "Unknown screen section directive '$word'"; } } return @screens; } sub read_tables { my $parser = shift; my $line; my @tables; while ($line = $parser->readline()) { push (@tables, $line =~ /(\w+)/g); } return [ @tables ]; } sub read_attributes { my $parser = shift; my $line; my %fields; my $lines = ''; while ($line = $parser->readline()) { chomp $line; $lines .= ' ' . $line; next unless $line =~ /;/; my ($name, $cols, $ignore, $ignore1, $ignore2, $attrs) = $lines =~ /\s*(\w+)((\s*=\s*\*?\w+\.\w+(\s*\[\d+,\d+\])?)+)\s*(\,\s*(.*))?;/; $attrs = '' unless defined($attrs); my $collist = []; foreach my $colspec (split /\s*=\s*/, $cols) { next unless $colspec; my ($verify, $tbl, $col, $subfield) = $colspec =~ /(\*)?(\w+)\.(\w+)(\s*\[\d+,\d+\])?/; my $subf; if ($subfield) { my ($st, $en) = $subfield =~ /\[(\d+),(\d+)\]/; $subf = [$st, $en]; } push @$collist, [$tbl, $col, $verify, $subf]; } my $attrhash = {}; while($attrs =~ /\s*(\w+)\s*((\w+)?\s*=\s*(\"[^\"]*\"|\((\s*\"[^\"]*\"\s*,?)*\)|(\w+)\s+JOINING\s+(\*?)(\w+)\.(\w+)|\w+))?,?/g ) { my $atname = uc $1; if (uc($atname) eq 'LOOKUP') { my $lfield = $3; my $lcol = $6; my $star = $7; my $ltable = $8; my $jcol = $9; $$attrhash{$atname} = [$lfield, $lcol, $star, $ltable, $jcol]; warn "LOOKUP table $ltable not in tables list" unless grep {$_ eq $ltable} @$TABLES; #warn "LOOKUP field $lfield not in fields list" # unless 0; # FIX ME! Look up in screens! } else { my $atval = $4; $atval =~ s/^\"(.*)\"$/$1/; $$attrhash{$atname} = defined($atval) ? $atval : 1; if ($5) { # list entry # digest list-valued attribute here. if ($atval =~ /^\s*\((\s*\"[^\"]*\",?)*\s*\)\s*$/) { my @vals = $atval =~ /\"([^\"]*)\"/g; my $hash = @vals && +{ map { ($_, 1) } @vals }; $$attrhash{"${atname}HASH"} = $hash if $hash; } } } } # special notice for INCLUDE warn "INCLUDE attribute (for field $name) must be a list of " . "double-quoted strings." if ($$attrhash{'INCLUDE'} && !$$attrhash{INCLUDEHASH}); $fields{$name} = [$collist, $attrhash]; $lines = ''; } return { %fields }; } sub read_instructions { my $parser = shift; my $line; my $instrs = {}; INSTRUCTION: while ($line = $parser->readline()){ next if $line =~ /^\s*$/; last if $line =~ /^\s*end\s*$/i; if ($line =~ /^\s*(\w+)\s+master\s+of\s+(\w+)/i) { push (@{$$instrs{MASTERS}}, [$1, $2]); } elsif ($line =~ /^\s*(before|after)\s+((\w+)\s+)+of\s+(\w+)\s*$/){ # control block my $when = $1; my $ops = $2; my $col = $4; my $action; while ($action = $parser->readline()) { last if $action =~ /^\s*$/; last INSTRUCTION if $action =~ /^\s*end\s*$/i; my @action; if ((@action = $action =~ /\s*(let)\s+(\w+)\s*=\s*(\w+)\s*([-+*\/])\s*(\w+)/i) || (@action = $action =~ /(nextfield)\s*=\s*(\w+)/i)) { $action[0] = lc($action[0]); my $actionref = [ @action ]; while ($ops =~ /(\w+)/g) { my $op = $1; push (@{$$instrs{CONTROLS}{$col}{$op}{$when}}, $actionref); } } else { warn "Unrecognized control block action: $action"; warn "(only let field = field + field and nextfield = field" ." are supported at this time)"; } } } else { warn "Unrecognized instruction line:\n$line\n"; } } return $instrs; } sub outputstring { my $db = shift; my $screens = shift; my $tables = shift; my $attrs = shift; my $instrs = shift; my $form = { db => $db, screens => $screens, tables => $tables, attrs => $attrs, instrs => $instrs }; my @strs = Data::Dumper->Dump([$form], ['form']); return join ($/, 'our $form;', @strs); } =head2 digest_file digest_file input_filename [output_filename] Reads the perform spec file, and writes a Perl Perform Spec file with the same basename but extension .pps unless an output filename is explicitly provided. Calls "digest" in this package to do the work. It\'s a little clumsy, but one can do a command-line "digestion" by: perl -MDBIx::Perform::DigestPer -e'digest_file "foo.per"' . Maybe a top-level Perl or shell script should be made for this purpose. =cut sub digest_file { my $infile = shift; my $outfile = shift; unless ($outfile ) { $outfile = $infile; $outfile .= ".pps" unless $outfile =~ s/\..*$/.pps/; } open (IN, "< $infile") or die "Couldn't open '$infile' for reading: $!"; open (OUT, "> $outfile") or die "Couldn't open '$outfile' for writing: $!"; my ($str) = digest(*IN); print OUT $str; print OUT "\n1;\n"; # let it be require'd close(OUT); } # Our little word muncher... package DBIx::Perform::DigestPer::Parser; sub new { my $class = shift; my $ioh = shift; my $self = bless {}, $class; $self->{'ioh'} = $ioh; $self->{'tail'} = ''; return $self; } # maybe "read token" would be a better description for this... sub readword { my $self = shift; my $accept_header_word = shift; my ($ioh, $tail) = @$self{'ioh','tail'}; do { my ($word) = $tail =~ /(\w+|[^\w\s]+)/; if ($word) { return undef if $DBIx::Perform::DigestPer::HEADING_WORDS{lc($word)} && lc($word) ne 'end' && ! $accept_header_word; $self->{'tail'} = $'; #' return $word; } $tail = <$ioh>; chomp $tail; $self->{'tail'} = $tail; } while (defined($tail)); return undef; } sub unread_word { my $self = shift; my $word = shift; $self->{'tail'} = $word . $self->{'tail'}; } sub readline { my $self = shift; my $accept_heading_word = shift; my $tail = $self->{'tail'}; return undef if $tail =~ /^\s*(\w+)\s*$/ && $DBIx::Perform::DigestPer::HEADING_WORDS{lc($1)} && lc($1) ne 'end' && !$accept_heading_word; $self->{'tail'} = ''; return $tail if ($tail =~ /\S/); my $ioh = $self->{'ioh'}; my $line = <$ioh>; return undef unless defined($line); chomp $line; return (($self->{'tail'} = $line) && undef) if $line =~ /^\s*(\w+)\s*$/ && $DBIx::Perform::DigestPer::HEADING_WORDS{lc($1)} && lc($1) ne 'end' && !$accept_heading_word; return $line eq '' ? ' ' : $line; } 1;