home *** CD-ROM | disk | FTP | other *** search
/ PC Professionell 2004 December / PCpro_2004_12.ISO / files / webserver / xampp / xampp-perl-addon-1.4.9-installer.exe / DBRecordGroup.pm < prev    next >
Encoding:
Perl POD Document  |  2001-11-05  |  18.0 KB  |  673 lines

  1. ############################################################################
  2. #
  3. # Win32::ASP::DBRecordGroup - an abstract parent class for representing
  4. #        groups of database database records in the Win32-ASP-DB system
  5. #
  6. # Author: Toby Everett
  7. # Revision: 0.02
  8. # Last Change:
  9. ############################################################################
  10. # Copyright 1999, 2000 Toby Everett.  All rights reserved.
  11. #
  12. # This file is distributed under the Artistic License. See
  13. # http://www.ActiveState.com/corporate/artistic_license.htm or
  14. # the license that comes with your perl distribution.
  15. #
  16. # For comments, questions, bugs or general interest, feel free to
  17. # contact Toby Everett at teverett@alascom.att.com
  18. ############################################################################
  19.  
  20. package Win32::ASP::DBRecordGroup;
  21. use Error qw/:try/;
  22. use Win32::ASP::Error;
  23.  
  24. use strict vars;
  25.  
  26. =head1 NAME
  27.  
  28. Win32::ASP::DBRecordGroup - an abstract parent class for representing groups of database records
  29.  
  30. =head1 SYNOPSIS
  31.  
  32. =head1 DESCRIPTION
  33.  
  34. The main purpose of C<Win32::ASP::DBRecordGroup>is to be subclassed.  It implements a generic set
  35. of default behavior for the purpose of reading a group of records from a table, displaying that
  36. group of records in an HTML table, and allowing edits to that group if applicable.  All
  37. C<Win32::ASP::DBRecordGroup> classes rely upon a C<Win32::ASP::DBRecord> class that implements the
  38. underlying record.
  39.  
  40. =head2 Internal Data Structure
  41.  
  42. The internal data structure of a instance of C<Win32::ASP::DBRecordGroup> consists of the
  43. following elements, all optional:
  44.  
  45. =over 4
  46.  
  47. =item orig
  48.  
  49. This is a reference to an array of DBRecord objects storing data read from the database.
  50.  
  51. =item edit
  52.  
  53. This is a reference to an array of DBRecord objects storing the currently modified data.
  54.  
  55. =back
  56.  
  57. =head2 Class Methods
  58.  
  59. Class methods were used to implement access to class properties.  Since Perl doesn't enforce a
  60. distinction between class and instance methods, these methods can be called on both class names
  61. and on instances of the class, which ends up being incredibly useful.  I strongly recommend against
  62. ever calling these methods using subroutine notation (i.e. C<&_DB> or C<&_PRIMARY_KEY>).
  63. Perl methods execute in the namespace in which they were defined, which means that if you further
  64. subclass and define a new implementation of those methods, any methods you don't override that were
  65. in the parent class will call the parent class's versions of those methods.  That's bad.  Always
  66. call these methods with the arrow notation and you'll be safe.
  67.  
  68. =head3 Mandatory Class Methods
  69.  
  70. These class methods will be overridden in every child class.
  71.  
  72. =over 4
  73.  
  74. =item _DB
  75.  
  76. The C<_DB> method should return the C<Win32::ASP::DB> (or subclass there-of) object that is used
  77. for database access.  A frequent implementation looks like this:
  78.  
  79.   sub _DB {
  80.     return $main::TheDB;
  81.   }
  82.  
  83. =cut
  84.  
  85. sub _DB {
  86.   return;
  87. }
  88.  
  89. =item _TYPE
  90.  
  91. The C<_TYPE> method should return the name of the C<Win32::ASP::DBRecord> subclass that implements
  92. the underlying records for this DBRecordGroup object.
  93.  
  94. =cut
  95.  
  96. sub _TYPE {
  97.   return 'MyStuff::MyRecord';
  98. }
  99.  
  100. =item _QUERY_METAS
  101.  
  102. The C<_QUERY_METAS> method should return a reference to a hash of subroutines that implement more
  103. complicated querying behavior.  The subroutines will be passed the appropriate query specification
  104. and should return legal SQL for inclusion within a query.  Of note, for performance reasons the
  105. method is usually implemented like so:
  106.  
  107.   sub _QUERY_METAS {
  108.     return $MyStuff::MyRecordGroup::query_metas;
  109.   }
  110.  
  111.   $MyStuff::MyRecordGroup::query_metas = {
  112.  
  113.     Status => sub {
  114.       my($values) = @_;
  115.       $values or return;
  116.       return "Status LIKE '[$values]'";
  117.     },
  118.  
  119.   };
  120.  
  121. The above C<Status> query_meta presumes a single character status field and that the passed value
  122. indicates a list of desired status values.  For instance, the status values might be C<N> for new
  123. records, C<P> for in process records, and C<F> for finished records.  Using the above query_meta,
  124. a user could query for C<Status=NP>, which would indicate they desired new and in process records.
  125. They could get the same results by querying for C<!F>.
  126.  
  127. Note that there is a security hole in the above code - if a user queries for C<Status=N] GO
  128. do_something_ugly_here>, they will effectively "jump" out of the LIKE statement and may be able to
  129. execute arbitrary SQL code.  Defending against this possibility is left as an excercise for the
  130. reader.
  131.  
  132. =cut
  133.  
  134. sub _QUERY_METAS {
  135.   return $Win32::ASP::DBRecordGroup::query_metas;
  136. }
  137.  
  138. $Win32::ASP::DBRecordGroup::query_metas = {
  139.  
  140. };
  141.  
  142.  
  143. =back
  144.  
  145. =head3 Optional Class Methods
  146.  
  147. These class methods can be overriden in a child class.
  148.  
  149. =over 4
  150.  
  151. =item _MIN_COUNT
  152.  
  153. The C<_MIN_COUNT> method defines the minimum number of records to display when allowing the user
  154. to edit a group of records.
  155.  
  156. =cut
  157.  
  158. sub _MIN_COUNT {
  159.   return 4;
  160. }
  161.  
  162. =item _NEW_COUNT
  163.  
  164. The C<_NEW_COUNT> method defines the minimum number of blank records to display when allowing the
  165. user to edit a group of records.
  166.  
  167. =cut
  168.  
  169. sub _NEW_COUNT {
  170.   return 2;
  171. }
  172.  
  173.  
  174. =back
  175.  
  176. =back
  177.  
  178. =head2 Instance Methods
  179.  
  180. =head3 new
  181.  
  182. This is a basic C<new> method.  It simply creates an anonymous hash and returns a reference.  The
  183. C<new> method is not responsible for reading data or anything else.  Just creating a new record
  184. group object.  You will probably not need to override this method.
  185.  
  186. =cut
  187.  
  188. sub new {
  189.   my $class = shift;
  190.  
  191.   my $self = {
  192.   };
  193.   bless $self, $class;
  194.   return $self;
  195. }
  196.  
  197. =head3 query
  198.  
  199. This is the heart of the DBRecordGroup class.  The method is passed three parameters: a reference
  200. to a hash of constraints, a string specifying how to order the results, and a string specifying a
  201. list of columns to retrieve.
  202.  
  203. The hash of constraints should be indexed on the field name (or query_meta name).  If the index
  204. references a query_meta, the value will be passed to the query_meta subroutine.  If the index
  205. doesn't reference a query_meta, the field will be tested for equality with the specified value.
  206. The specified value will be formatted by the field's C<as_sql> method before being included in the
  207. SQL.  All of the constraints will be ANDed together to form the C<WHERE> clause in the SQL.
  208.  
  209. The order string should be a comma separated list of field names.  Bare field names will be sorted
  210. in ascending order; field names preceded by a minus sign will be sorted in descending order.
  211.  
  212. The columns string should be one of three things: empty, an asterisk, or a comma separated list of
  213. field names.  If the string is absent or an asterisk, the query will retrieve all the columns  If
  214. a comma separated list is specified, the query will only retrieve those columns.  The advantage of
  215. this is that queries can be optimized to return only the information that will be displayed to the
  216. user.  Keep in mind, however, that if the DBRecord object requires specific fields in order to
  217. make determinations about viewability or the like, those columns need to be specified in the
  218. column list.  As a result, C<query> is frequently overriden to automatically append those columns
  219. to the column list before call C<SUPER::query>.
  220.  
  221. After setting up the SQL for the query, C<query> calls C<exec_sql> on the appropriate
  222. Win32::ASP::DB object (determined by calling C<< $self->_DB >>).  It then iterates over the result
  223. set returned, creating new DBRecord objects of the appropriate class and calling C<_read> on them.
  224. The call to C<_read> is wrapped in a C<try> block - if the user doesn't have rights to view the
  225. record, C<_read> will throw an exception.  That exception will be trapped and the record won't be
  226. be appended to the array of DBRecord objects.
  227.  
  228. Another common modification to C<query> involves adding constraints to all queries to explicitly
  229. call query_metas that are responsible for ascertaining viewability.  This can greatly improve
  230. performance - if the user asks for every record in the system, the query handles the weeding out
  231. of those records that are not viewable, rather than reading the data and letting C<_read> throw an
  232. exception.  Again, this can be easily handled by overriding the method and then calling
  233. C<SUPER::query>.
  234.  
  235. =cut
  236.  
  237. sub query {
  238.   my $self = shift;
  239.   my($ref2constraints, $order, $columns) = @_;
  240.  
  241.   exists ($self->{orig}) and return;
  242.  
  243.   my $ref2columns;
  244.   if (!defined $columns or $columns !~ /\S/ or $columns =~ /\*/) {
  245.     $columns = '*';
  246.   } else {
  247.     my %columns;
  248.     %columns = map {$_, 1} grep {/\S/} split(/,/, $columns);
  249.     $columns = join(', ', sort keys %columns);
  250.     $ref2columns = \%columns;
  251.   }
  252.  
  253.   my(@constraints);
  254.   foreach my $field (keys %{$ref2constraints}) {
  255.     my $value = $ref2constraints->{$field};
  256.     $value eq '' and next;
  257.     if (exists($self->_QUERY_METAS->{$field})) {
  258.       push(@constraints, &{$self->_QUERY_METAS->{$field}}($value));
  259.     } else {
  260.       push(@constraints, "$field = ".$self->_TYPE->_FIELDS->{$field}->as_sql($value));
  261.     }
  262.   }
  263.   my $constraints = join(" AND\n    ", grep(/\S/, @constraints));
  264.   $constraints and $constraints = 'WHERE '.$constraints;
  265.  
  266.   my(@order) = split(/,/, $order);
  267.   foreach my $i (@order) {
  268.     $i =~ /^(-?)(.+)$/ or
  269.         throw Win32::ASP::Error::DBRecordGroup::bad_order (order => $i);
  270.     my($asc, $field) = ($1, $2);
  271.     exists $self->_TYPE->_FIELDS->{$field} or
  272.         throw Win32::ASP::Error::Field::non_existent (fieldname => $field, method => 'Win32::ASP::DBRecordGroup::query');
  273.     $asc eq '-' and $asc = ' DESC';
  274.     $i = $field.$asc;
  275.   }
  276.   $order = join(" ,\n    ", @order);
  277.   $order and $order = 'ORDER BY '.$order;
  278.  
  279.   my $SQL = "SELECT $columns FROM ".$self->_TYPE->_READ_SRC."\n$constraints\n$order";
  280.   my $results = $self->_DB->exec_sql($SQL);
  281.  
  282.   until ($results->EOF) {
  283.     my $record = $self->_TYPE->new;
  284.     try {
  285.       $record->_read($results, $ref2columns);
  286.       $record->{parent} = $self;
  287.       push(@{$self->{orig}}, $record);
  288.     } otherwise {};
  289.     $results->MoveNext;
  290.   }
  291. }
  292.  
  293.  
  294. =head3 query_deep
  295.  
  296. =cut
  297.  
  298. sub query_deep {
  299.   my $self = shift;
  300.   my($ref2constraints, $order, $columns) = @_;
  301.  
  302.   $self->query($ref2constraints, $order, $columns);
  303.  
  304.   scalar(@{$self->{orig}}) or return;
  305.  
  306.   my(@PRIMARY_KEY) = $self->_TYPE->_PRIMARY_KEY;
  307.   my(@PRIMARY_SHT) = @PRIMARY_KEY;
  308.   my $PRIMARY_LST = pop(@PRIMARY_SHT);
  309.  
  310.   my $index_hash = {};
  311.   foreach my $i (0..$#{$self->{orig}}) {
  312.     my $temp = $index_hash;
  313.     foreach my $field (@PRIMARY_SHT) {
  314.       $temp = $index_hash->{$self->{orig}->[$i]->{orig}->{$field}};
  315.     }
  316.     $temp->{$self->{orig}->[$i]->{orig}->{$PRIMARY_LST}} = $i;
  317.   }
  318.  
  319.   foreach my $child (keys %{$self->_TYPE->_CHILDREN}) {
  320.     my($type, $pkext) = @{$self->_TYPE->_CHILDREN->{$child}}{'type', 'pkext'};
  321.  
  322.     foreach my $i (0..$#{$self->{orig}}) {
  323.       $self->{orig}->[$i]->{$child} = $type->new;
  324.       $self->{orig}->[$i]->{$child}->{parent} = $self->{orig}->[$i];
  325.     }
  326.  
  327.     my $temp = $type->new;
  328.     $temp->query($ref2constraints, $pkext);
  329.  
  330.     my $record;
  331.     while ($record = shift @{$temp->{orig}}) {
  332.       my $temp = $index_hash;
  333.       foreach my $field (@PRIMARY_SHT) {
  334.         $temp = $index_hash->{$record->{orig}->{$field}};
  335.       }
  336.       exists $temp->{$record->{orig}->{$PRIMARY_LST}} or next;
  337.       my $index = $temp->{$record->{orig}->{$PRIMARY_LST}};
  338.  
  339.       $record->{parent} = $self->{orig}->[$index]->{$child};
  340.       push(@{$self->{orig}->[$index]->{$child}->{orig}}, $record);
  341.     }
  342.   }
  343. }
  344.  
  345. =head3 index_hash
  346.  
  347. =cut
  348.  
  349. sub index_hash {
  350.   my $self = shift;
  351.  
  352.   my $retval = {};
  353.   foreach my $i (0..$#{$self->{orig}}) {
  354.     $retval->{$self->{orig}->[$i]->{orig}->{ChangeID}} = $i;
  355.   }
  356.  
  357.   return $retval;
  358. }
  359.  
  360. =head3 insert
  361.  
  362. =cut
  363.  
  364. sub insert {
  365.   my $self = shift;
  366.  
  367.   foreach my $i (0..$#{$self->{edit}}) {
  368.     try {
  369.       $self->{edit}->[$i]->insert;
  370.     } otherwise {
  371.       my $E = shift;
  372.       throw Win32::ASP::Error::Field::group_wrapper (E => $E, row_type => $self->_TYPE->_FRIENDLY, row_id => $i+1, activity => 'insert');
  373.     };
  374.   }
  375. }
  376.  
  377. =head3 delete
  378.  
  379. =cut
  380.  
  381. sub delete {
  382.   my $self = shift;
  383.  
  384.   foreach my $i (@{$self->{edit}}) {
  385.     $i->delete;
  386.   }
  387. }
  388.  
  389. =head3 should_update
  390.  
  391. =cut
  392.  
  393. sub should_update {
  394.   my $self = shift;
  395.  
  396.   if ($self->merge_inner) {
  397.     my $retval = 1;
  398.     foreach my $i (@{$self->{orig}}) {
  399.       $i->should_update or $retval = 0;
  400.     }
  401.     $self->split_inner;
  402.     return $retval;
  403.   } else {
  404.     return 0;
  405.   }
  406. }
  407.  
  408. =head3 update
  409.  
  410. =cut
  411.  
  412. sub update {
  413.   my $self = shift;
  414.  
  415.   if ($self->should_update) {
  416.     $self->merge_inner;
  417.     foreach my $i (0..$#{$self->{orig}}) {
  418.       try {
  419.         $self->{orig}->[$i]->update;
  420.       } otherwise {
  421.         my $E = shift;
  422.         throw Win32::ASP::Error::Field::group_wrapper (E => $E, row_type => $self->_TYPE->_FRIENDLY, row_id => $i+1, activity => 'update');
  423.       };
  424.     }
  425.     $self->split_inner;
  426.     return 0;
  427.   } else {
  428.     foreach my $i (@{$self->{orig}}) {
  429.       $i->delete;
  430.     }
  431.  
  432.     foreach my $i (0..$#{$self->{edit}}) {
  433.       try {
  434.         $self->{edit}->[$i]->insert;
  435.       } otherwise {
  436.         my $E = shift;
  437.         throw Win32::ASP::Error::Field::group_wrapper (E => $E, row_type => $self->_TYPE->_FRIENDLY, row_id => $i+1, activity => 'update');
  438.       };
  439.     }
  440.     return 1;
  441.   }
  442. }
  443.  
  444. =head3 edit
  445.  
  446. =cut
  447.  
  448. sub edit {
  449.   my $self = shift;
  450.  
  451.   unless (exists $self->{edit}) {
  452.     foreach my $i (@{$self->{orig}}) {
  453.       $i->edit;
  454.     }
  455.     $self->split_inner;
  456.   }
  457. }
  458.  
  459. =head3 merge_inner
  460.  
  461. =cut
  462.  
  463. sub merge_inner {
  464.   my $self = shift;
  465.  
  466.   if ($#{$self->{orig}} == $#{$self->{edit}}) {
  467.     foreach my $i (0..$#{$self->{orig}}) {
  468.       $self->{orig}->[$i]->merge($self->{edit}->[$i]);
  469.     }
  470.     delete $self->{edit};
  471.     return 1;
  472.   } else {
  473.     return 0;
  474.   }
  475. }
  476.  
  477. =head3 split_inner
  478.  
  479. =cut
  480.  
  481. sub split_inner {
  482.   my $self = shift;
  483.  
  484.   $self->{edit} = [];
  485.   foreach my $i (@{$self->{orig}}) {
  486.     push(@{$self->{edit}}, $i->split);
  487.   }
  488. }
  489.  
  490. =head3 post
  491.  
  492. =cut
  493.  
  494. sub post {
  495.   my $self = shift;
  496.   my($column) = @_;
  497.  
  498.   exists $self->_TYPE->_FIELDS->{$column} or
  499.       throw Win32::ASP::Error::Field::non_existent (fieldname => $column, method => 'Win32::ASP::DBRecordGroup::post');
  500.   my $count = $main::Request->Form($self->_TYPE->_FIELDS->{$column}->formname)->Count;
  501.  
  502.   my $orow = 0;
  503.   foreach my $irow (1..$count) {
  504.     my $record = $self->_TYPE->new;
  505.     $record->post($irow);
  506.     if ($record->row_check($orow)) {
  507.       $record->{parent} = $self;
  508.       push(@{$self->{edit}}, $record);
  509.       $orow++;
  510.     }
  511.   }
  512. }
  513.  
  514. =head3 add_extras
  515.  
  516. =cut
  517.  
  518. sub add_extras {
  519.   my $self = shift;
  520.  
  521.   my $new;
  522.   my $min_count = $self->_MIN_COUNT;
  523.   my $new_count = $self->_NEW_COUNT;
  524.   defined $min_count && defined $new_count or return;
  525.  
  526.   my $cur_count = scalar(@{$self->{edit}});
  527.   $cur_count < $min_count and $new = $min_count - $cur_count;
  528.   $new < $new_count and $new = $new_count;
  529.  
  530.   foreach my $i (1..$new) {
  531.     my $record = $self->_TYPE->new;
  532.     $record->{parent} = $self;
  533.     $record->init;
  534.     push(@{$self->{edit}}, $record);
  535.   }
  536. }
  537.  
  538. =head3 set_prop
  539.  
  540. =cut
  541.  
  542. sub set_prop {
  543.   my $self = shift;
  544.   my($fieldname, $value) = @_;
  545.  
  546.   foreach my $i (@{$self->{edit}}) {
  547.     $i->{edit}->{$fieldname} = $value;
  548.   }
  549. }
  550.  
  551. =head3 gen_table
  552.  
  553. =cut
  554.  
  555. sub gen_table {
  556.   my $self = shift;
  557.   my($columns, $data, $viewtype, %params) = @_;
  558.  
  559.   $viewtype eq 'edit' and $self->add_extras;
  560.  
  561.   my(@columns) = split(/,/, $columns);
  562.  
  563.   foreach my $field (@columns) {
  564.     exists $self->_TYPE->_FIELDS->{$field} or
  565.         throw Win32::ASP::Error::Field::non_existent (fieldname => $field, method => 'Win32::ASP::DBRecordGroup::gen_table');
  566.   }
  567.  
  568.   my $retval = <<ENDHTML;
  569. <TABLE border="1" cellpadding="3" bordercolordark="#000000" bordercolorlight="#000000">
  570.   <TR>
  571. ENDHTML
  572.  
  573.   foreach my $field (@columns) {
  574.     $retval .= "    <TH>".$self->_TYPE->_FIELDS->{$field}->desc."</TH>\n";
  575.   }
  576.   $retval .= "  </TR>\n";
  577.  
  578.   foreach my $record (@{$self->{$data}}) {
  579.     $retval .= "  <TR>\n";
  580.     foreach my $field (@columns) {
  581.       $retval .= "    <TD valign=\"top\">";
  582.       my $temp;
  583.       $temp = $record->field($field, $data, $viewtype);
  584.       if ($viewtype eq 'view' and $params{active} eq $field) {
  585.         $temp = "<A HREF=\"$params{activedest}=$record->{$data}->{$field}\">$temp</A>";
  586.       }
  587.       $retval .= $temp."</TD>\n";
  588.     }
  589.     $retval .= "  </TR>\n";
  590.   }
  591.   $retval .= "</TABLE>\n";
  592.  
  593.   return $retval;
  594. }
  595.  
  596. =head3 get_QS_constraints
  597.  
  598. =cut
  599.  
  600. sub get_QS_constraints {
  601.   my(%constraints);
  602.  
  603.   my $count = $main::Request->QueryString('constraint')->{Count};
  604.   foreach my $i (1..$count) {
  605.     my $constraint = $main::Request->QueryString('constraint')->Item($i);
  606.     $constraint =~ /^([^=]+)=([^=]*)$/ or
  607.         throw Win32::ASP::Error::DBRecordGroup::bad_constraint (constraint => $constraint);
  608.     $constraints{$1} = $2;
  609.   }
  610.   return %constraints;
  611. }
  612.  
  613. =head3 make_QS_constraints
  614.  
  615. =cut
  616.  
  617. sub make_QS_constraints {
  618.   my(%constraints) = @_;
  619.  
  620.   return map {return (constraint => "$_=$constraints{$_}")} keys %constraints;
  621. }
  622.  
  623. =head3 debug_dump
  624.  
  625. =cut
  626.  
  627. sub debug_dump {
  628.   my $self = shift;
  629.  
  630.   $main::Response->Write("<XMP>".Data::Dumper->Dump([$self], ['self'])."</XMP>");
  631. }
  632.  
  633.  
  634.  
  635. ####################### Error Classes ##################################333
  636.  
  637. package Win32::ASP::Error::DBRecordGroup;
  638. @Win32::ASP::Error::DBRecordGroup::ISA = qw/Win32::ASP::Error/;
  639.  
  640.  
  641. package Win32::ASP::Error::DBRecordGroup::bad_constraint;
  642. @Win32::ASP::Error::DBRecordGroup::bad_constraint::ISA = qw/Win32::ASP::Error::DBRecordGroup/;
  643.  
  644. #Parameters:  constraint
  645.  
  646. sub _as_html {
  647.   my $self = shift;
  648.  
  649.   my $constraint = $self->constraint;
  650.   return <<ENDHTML;
  651. Improperly formed constraint "$constraint".<P>
  652. ENDHTML
  653. }
  654.  
  655.  
  656.  
  657. package Win32::ASP::Error::DBRecordGroup::bad_order;
  658. @Win32::ASP::Error::DBRecordGroup::bad_order::ISA = qw/Win32::ASP::Error::DBRecordGroup/;
  659.  
  660. #Parameters:  order
  661.  
  662. sub _as_html {
  663.   my $self = shift;
  664.  
  665.   my $order = $self->order;
  666.   return <<ENDHTML;
  667. Improperly formed order element "$order".<P>
  668. ENDHTML
  669. }
  670.  
  671.  
  672. 1;
  673.