#!/usr/bin/perl #---------------------------------------------------------------- # ars-gen-subclass # # queries ARS and generates a subclass module from a schema. The # parameter '-e' can be used to specify an existing subclass module # to be 'edited', which really means the old subclass module is read # beforehand and customizations are noted and inserted into the new # subclass module. # # Things preserved by edit-mode: # 1. package name # 2. the 'query' value for individual fields # 3. all methods between the 'START CUSTOMIZATION' and 'END CUSTOMIZATION' # markers #---------------------------------------------------------------- use FileHandle; use ARS; use ArsBaseClass; #---------------------------------------------------------------- # make sure you have set $ARSUSER, $ARSPASS, or $ARSSERVER in. # ArsBaseClass.pm properly. # ars-gen-subclass uses the CTL object from ArsBaseClass.pm # change the corresponding variables there. #---------------------------------------------------------------- my $BASECLASSNAME = 'ArsBaseClass'; my $MODIFY_TIME = 6; my $DEBUG = 0; my $CTL; my $METHOD_STYLE = 'us'; # can be 'us' or 'ucfirst' # us = underscore: this_is_a_method_name # ucfirst = uppercase first: thisIsAMethodName $query_field = 'undef, # EDIT ME'; $query_field2 = 'undef, # EDIT ME'; my $comment_block = <<_EOF_; #---------------------------------------------------------------- # the CLASS_DATA structure was generated automatically using # "ars-gen-subclass". It has many porpoises: # # 1. it maps my private field-keys to the official ones # (example: private = "status_confirmed", official = "Status Confirmed") # # 2. it describes the fields as ARS knows them. This makes it # possible to perform a "sanity check" later as insurance that # the ARS data-structure hasn't been secretly changed. # # 3. it determines whether fields will be retrieved during a query. # This is controlled, oddly enough, by setting "query => 1" to # retrieve. # # the keys to the hash are as follows: # # schema => 'the-name-of-the-schema', # query_field => 'the fieldname to be used for finding a single record', # query_field2 => 'the second fieldname to be used for finding a record', # priv_keys => { # "private-key" => { # real => "real-key", # id => ARS-field-id, # type => (char/enum/time/diary), # query => 0|1, # } # }, # # # this part is generated automatically # reverse_map => { # 'ARS-field-id' => 'private-key', # } # # if type is "enum", there is an additional hash-key "enum": # enum => { # "private-enum-val-1" => [ internal-position, "ARS-name" ], # "private-enum-val-2" => [ internal-position, "ARS-name" ], # "private-enum-val-n" => [ internal-position, "ARS-name" ], # } # # CHANGES: # # 7-MAY-2004: # # - added default function "query()" to generated class to # allow generic (sort-of) queries # - generated class now contains start/end markers for customized # code. # - added ability to "edit" an existing generated class file, which # really means the old class file is read and the "interesting" # information is saved to re-generate the class: # - package name # - the designated query_field # - the "query" settings for all fields # - any customized code #---------------------------------------------------------------- _EOF_ $edit = undef; $backup = 0; while($_ = shift) { if (/^-e/) { $edit = shift; } elsif (/^-b/) { $backup = 1; } elsif (!$schema) { $schema = $_; } else { die "usage: $0 [-b] [-e ] schema\n"; } } if (!$schema) { exit; } if ($edit) { (-e $edit) || die "file [$edit] not found\n"; if ($backup) { backupFile($edit); } $fh = FileHandle->new($edit); defined($fh) || die "open of [$edit] for reading failed\n"; $saving = 0; $in_struct = 0; while(<$fh>) { $DEBUG && print "DBG> $_"; if ($in_struct) { if (/^};/) { $in_struct = 0; next; } if (/^\s+query_field\s+=>\s+([^,]+,)(\s+#\s*EDIT\s+ME)?\s*$/) { $query_field = $1; } if (/^\s+query_field2\s+=>\s+([^,]+,)(\s+#\s*EDIT\s+ME)?\s*$/) { $query_field2 = $1; } if (/^\t\t'([^']+)'\s+=>\s+{/) { $curr_field = $1; $DEBUG && print "curr_field = [$curr_field]\n"; } if (/^\t\t\tquery\s+=>\s+(\d+),/) { $query_values{$curr_field} = $1; $DEBUG && print "saving query{$curr_field} = $1\n"; } } else { if (/^my \$CLASS_DATA = {/) { $in_struct = 1; next; } if (/^package\s+(\S+);/) { $old_package = $1; } } if ($saving) { if (/^#\sEND\sCUSTOMIZATION/) { $saving = 0; $DEBUG && print " -stop saving-\n"; last; } $DEBUG && print " -saving-\n"; $customized .= $_; } else { if (/^#\sSTART\sCUSTOMIZATION/) { $DEBUG && print " -start saving-\n"; $saving = 1; } } } $fh->close; } $CTL = ArsBaseClass::_arsControl; # get field info (%fields = ars_GetFieldTable($CTL, $schema)) || die "schema [$schema] seems not to exist.\n"; $package = packageName($schema); if ($edit) { $fh = FileHandle->new($edit, 'w'); } else { $fh = FileHandle->new; open($fh, '>&STDOUT') || die "can't dup stdout"; } if ($edit) { print $fh "package ${old_package};\n\n"; } else { print $fh "package ${package}; # EDIT ME\n\n" } print $fh "use $BASECLASSNAME;\n\n\@ISA = '$BASECLASSNAME';\n\n" ."$comment_block\n\n" ."my \$CTL = undef;\n" ."my \$ARSSERVER = undef;\nmy \$ARSUSER = undef;\nmy \$ARSPASS = undef;\n\n" ."my \$CLASS_DATA = {\n" ."\tschema => '$schema',\n" ."\tquery_field => $query_field\n" ."\tquery_field2 => $query_field2\n" ."\tpriv_keys => {\n"; foreach $field (sort sortById keys(%fields)) { ($finfo = ars_GetField($CTL, $schema, $fields{$field})) || die $arr_errstr; $type = $finfo->{dataType}; $DEBUG && print STDERR "FIELD: $field, type=[$type]\n"; if ($type =~ /^(control|trim|page|table)$/) { $DEBUG && print STDERR "is control/trim/page/table, skipping.\n"; next; } $key = normalize($field, 'field'); my $real = $field; $real =~ s/'/\\'/g; if ($edit && exists($query_values{$key})) { $query = $query_values{$key}; } else { $query = ($type eq 'diary') ? 0 : 1; } if ($key eq 'write') { print "CONFLICT! you have a field named \"write\", which conflicts\n"; print "with an existing method. Renaming to \"write_conflict\"\n"; $key = 'write_conflict'; } my $gui_label = getGuiLabel($finfo); $gui_label =~ s/'/\\'/g; print $fh "\t\t'$key' => {\n" ."\t\t\treal\t=> '$real',\n" ."\t\t\tid\t\t=> '$fields{$field}',\n" ."\t\t\ttype\t=> '$type',\n" ."\t\t\tquery\t=> $query,\n" ."\t\t\tgui_label\t=> '$gui_label',\n"; #."\t\t\tquery\t=> 0,\n"; if ($type eq 'enum') { print $fh "\t\t\t# '$field' is of type enum, (special handing)\n"; print $fh "\t\t\tenum => {\n"; #if ($#{$finfo->{limit}} == 0) { exists($finfo->{limit}->{enumLimits}) || die "I'm not sure what to do with non-regularLists of enums"; my @values = @{$finfo->{limit}->{enumLimits}}; if ($#values == 0) { print $fh "\t\t\t\t'yes' => [ 0, '$values[0]' ],\n"; } else { $ind = 0; foreach $val (@values) { $ekey = normalize($val); $val =~ s/'/\\'/g; print $fh "\t\t\t\t'$ekey' => [ $ind, '$val' ],\n"; $ind++; } } print $fh "\t\t\t},\n"; } print $fh "\t\t},\n"; } print $fh "\t},\n};\n\n"; print $fh "# initialization\n${BASECLASSNAME}::init(\$CLASS_DATA);\n\n"; print $fh <<_EOF_; sub new { my \$proto = shift; my \$class = ref(\$proto) || \$proto; my \@args; if (!grep(/^CTL\$/, \@_) && \$CTL) { push(\@args, 'CTL', \$CTL); } my \$self = \$class->SUPER::new(\@args, \@_); if (!defined(\$self)) { return undef; } bless(\$self, \$class); } INIT { my \@params; \$ARSSERVER && push(\@params, SERVER => \$ARSSERVER); \$ARSUSER && push(\@params, USER => \$ARSUSER); \$ARSPASS && push(\@params, PASS => \$ARSPASS); if (\@params) { \$CTL = ArsBaseClass::login(\@params); } } sub import { shift; # get rid of \$class my \$word; while (\$word = shift) { if (\$word eq 'USER') { \$ARSUSER = shift; } elsif (\$word eq 'PASS') { \$ARSPASS = shift; } elsif (\$word eq 'SERVER') { \$ARSSERVER = shift; } } } sub _getClassData { \$CLASS_DATA; } sub schemaChanged { return ${BASECLASSNAME}::schemaChanged(\$CLASS_DATA); } sub query { my \@result = ArsBaseClass::query(\$CLASS_DATA, \@_); if (\@result) { return wantarray ? \@result : \$result[0]; } return wantarray ? () : undef; } # START CUSTOMIZATION ${customized}# END CUSTOMIZATION 1; _EOF_ exit; sub sortById { return $fields{$a} <=> $fields{$b}; } #-------------------------------------------- # generates package name from schema name #-------------------------------------------- sub packageName { my $schema = shift; $schema =~ s/([a-z])([A-Z])/$1 $2/g; $schema =~ tr/A-Z/a-z/; $schema =~ s/^\s*//; # remove WS at beginning $schema =~ s/\s*$//; # remove WS at end $schema =~ s/[^a-z0-9 ]+/ /g; # substitute WS for all non-word chars my @f = split(' ', $schema); my $package = join('', map(ucfirst($_), @f)); $package; } sub normalize { my $str = shift; my $use = shift; $str =~ s/([a-z])([A-Z])/$1 $2/g; $str =~ tr/A-Z/a-z/; $str =~ s/^\s*//; # remove WS at beginning $str =~ s/\s*$//; # remove WS at end if (($use eq 'field') && ($METHOD_STYLE eq 'ucfirst')) { $str =~ s/[^a-z0-9 ]/ /g; # substitute WS for all non-word chars my ($first, @rest) = split(' ', $str); $str = join('', $first, map(ucfirst($_), @rest)); } else { $str =~ s/[^a-z0-9_ ]/ /g; # substitute WS for all non-word chars $str =~ s/\s+/_/g; # condense multiple WS chars to single "_" $str =~ s/_*$//g; # remove trailing underscores } $str; } sub backupFile { my $file = shift; my $suffix = ''; my $name = "${file}.bak$suffix"; while(-e $name) { $suffix++; $name = "${file}.bak$suffix"; } my $cmd = "cp $file $name"; if (system($cmd)) { die "backupFile: cmd $cmd failed"; } } sub getGuiLabel { my $finfo = shift; # {displayInstanceList}->{dInstanceList}->[0]->{props}->[0] exists($finfo->{displayInstanceList}) || return undef; (ref($finfo->{displayInstanceList}) eq 'HASH') || return undef; exists($finfo->{displayInstanceList}->{dInstanceList}) || return undef; (ref($finfo->{displayInstanceList}->{dInstanceList}) eq 'ARRAY')|| return undef; exists($finfo->{displayInstanceList}->{dInstanceList}->[0]) || return undef; exists($finfo->{displayInstanceList}->{dInstanceList}->[0]->{props}) || return undef; (ref($finfo->{displayInstanceList}->{dInstanceList}->[0]->{props}) eq 'ARRAY') || return undef; foreach my $elem (@{$finfo->{displayInstanceList}->{dInstanceList}->[0]->{props}}) { (ref($elem) eq 'HASH') || die "expected ref to be HASH"; if ($elem->{prop} == 20) { # AR_DPROP_LABEL return $elem->{value}; } } }