1# ResultSet.pm - Will pull settings from an XML config file into a format usable by the system.
2# Created by James A. Pattie.  Copyright (c) 2001-2002, Xperience, Inc.
3
4package DBIWrapper::ResultSet;
5
6use strict;
7use XML::LibXML;
8use vars qw ($AUTOLOAD @ISA @EXPORT $VERSION);
9
10require Exporter;
11@ISA = qw(Exporter AutoLoader);
12@EXPORT = qw();
13
14$VERSION = "1.1";
15
16# new
17sub new
18{
19  my $that = shift;
20  my $class = ref($that) || $that;
21  my $self = bless {}, $class;
22  my $errStr = "ResultSet->new()  - Error:";
23
24  $self->{version} = "1.2";
25  $self->{resultFile} = "";
26  $self->{select} = 0;  # indicates if a <select /> tag was in the xml.
27  $self->{sql} = "";
28  $self->{plugs} = "";
29  $self->{result} = "";
30  $self->{error} = "";
31  $self->{columns} = "0";
32  $self->{numRows} = "0";
33  $self->{rows} = [];
34  $self->{columnNames} = [];  # the column names used.
35  $self->{columnNamesHash} = {};  # same as columnNames but for quicker lookups.
36  $self->{errorCodes} = {
37    0  => "version = '%s' is invalid",
38    1  => "resultFile must be specified",
39    2  => "sql must be specified",
40    3  => "result = '%s' is invalid",
41    4  => "columns = '%s' is invalid",
42    5  => "numRows = '%s' is invalid",
43    6  => "numRows = '%s' but rows = '%s'!",
44    7  => "no columnNames defined",
45    8  => "columnNames count != columnNamesHash",
46    9  => "column = '%s' in columnNamesHash, not in columnNames",
47    10 => "row = '%s' only has '%s' columns defined",
48    11 => "row = '%s', column = '%s' defined but not in columnNames!",
49  };
50
51  return $self;
52}
53
54sub AUTOLOAD
55{
56  my $self = shift;
57  my $type = ref($self) || die "$self is not an object";
58  my $name = $AUTOLOAD;
59  $name =~ s/.*://; # strip fully-qualified portion
60  unless (exists $self->{$name})
61  {
62    die "Can't access `$name' field in object of class $type";
63  }
64  if (@_)
65  {
66    return $self->{$name} = shift;
67  }
68  else
69  {
70    return $self->{$name};
71  }
72}
73
74sub isValid
75{
76  my $self = shift;
77  my @errors = ();
78
79  my $strict = 0;
80
81  if ($self->{version} !~ /^(1.2)$/)
82  {
83    push @errors, sprintf($self->{errorCodes}->{0}, $self->{version});
84  }
85  if (length $self->{resultFile} == 0)
86  {
87    push @errors, $self->{errorCodes}->{1};
88  }
89  if (length $self->{sql} == 0)
90  {
91    push @errors, $self->{errorCodes}->{2};
92  }
93  if ($self->{result} !~ /^(Ok|Error)$/)
94  {
95    push @errors, sprintf($self->{errorCodes}->{3}, $self->{result});
96  }
97  if ($self->{columns} !~ /^(0|1)$/)
98  {
99    push @errors, sprintf($self->{errorCodes}->{4}, $self->{columns});
100  }
101  if ($self->{numRows} !~ /^(\d+)$/)
102  {
103    push @errors, sprintf($self->{errorCodes}->{5}, $self->{numRows});
104  }
105  else
106  {
107    # validate that we have the expected number of rows.
108    if (scalar @{$self->{rows}} != $self->{numRows})
109    {
110      push @errors, sprintf($self->{errorCodes}->{6}, $self->{numRows}, scalar @{$self->{rows}});
111    }
112    if ($self->{numRows} > 0)
113    {
114      my $numColumns = scalar @{$self->{columnNames}};
115      if ($numColumns == 0)
116      {
117        push @errors, $self->{errorCodes}->{7};
118      }
119      else
120      {
121        if ($numColumns != scalar keys %{$self->{columnNamesHash}})
122        {
123          push @errors, $self->{errorCodes}->{8};
124        }
125        # now verify that all entries in the columnNamesHash exist in columnNames
126        foreach my $column (keys %{$self->{columnNamesHash}})
127        {
128          my $found = 0;
129          for (my $k=0; $k < scalar @{$self->{columnNames}} && !$found; $k++)
130          {
131            if ($self->{columnNames}->[$k] == $column)
132            {
133              $found = 1;
134            }
135          }
136          if (!$found)
137          {
138            push @errors, sprintf($self->{errorCodes}->{9}, $column);
139          }
140        }
141        # make sure each row has the correct number of columns defined.
142        for (my $i=0; $i < scalar @{$self->{rows}}; $i++)
143        {
144          my $rowRef = $self->{rows}->[$i];
145          if (scalar(keys %{$rowRef}) != $numColumns)
146          {
147            push @errors, sprintf($self->{errorCodes}->{10}, $i, scalar(keys %{$rowRef}));
148          }
149          else  # validate that all columns defined in the row exist in columnNames
150          {
151            foreach my $columnName (keys %{$self->{rows}->[$i]})
152            {
153              if (not exists $self->{columnNamesHash}->{$columnName})
154              {
155                push @errors, sprintf($self->{errorCodes}->{11}, $i, $columnName);
156              }
157            }
158          }
159        }
160      }
161    }
162  }
163
164  return ((scalar @errors > 0 ? 0 : 1), \@errors);
165}
166
167sub displayData
168{
169  my $self = shift;
170  my %args = ( style => "text", @_ );
171  my $style = $args{style};
172  my $result = "";
173  my $errStr = "ResultSet->displayData()  - Error:";
174
175  if ($style !~ /^(text|html)$/)
176  {
177    die "$errStr  style = '$style' is invalid!\n";
178  }
179
180  my @valid = $self->isValid();
181  if ($valid[0])
182  {
183    my $columnNames = "'" . join ("', '", @{$self->{columnNames}}) . "'";
184    if ($style eq "text")
185    {
186      $result .= "version = '$self->{version}'\n";
187      $result .= "resultFile = '$self->{resultFile}'\n";
188      $result .= "sql = '$self->{sql}'\n";
189      $result .= "plug = '$self->{plug}'\n";
190      $result .= "result = '$self->{result}'\n";
191      $result .= "error = '$self->{error}'\n";
192      $result .= "columns = '$self->{columns}'\n";
193      $result .= "numRows = '$self->{numRows}'\n";
194      $result .= "columnNames = '$columnNames'\n";
195      for (my $i=0; $i < $self->{numRows}; $i++)
196      {
197        $result .= "row $i: ";
198        for (my $j=0; $j < scalar @{$self->{columnNames}}; $j++)
199        {
200          my $column = $self->{columnNames}->[$j];
201          $result .= ", " if ($j > 0);
202          $result .= "'$column' = '$self->{rows}->[$i]->{$column}'";
203        }
204      }
205    }
206    elsif ($style eq "html")
207    {
208      my $resultFile = $self->encodeHTML(string => $self->{resultFile});
209
210      my $error = $self->encodeHTML(string => $self->{error});
211      my $sql = $self->encodeHTML(string => $self->{sql});
212      my $plug = $self->encodeHTML(string => $self->{plug});
213
214      $result .= <<"END_OF_CODE";
215
216<center><h1>ResultSet Version $self->{version}</h1></center>
217  <table border="1" cellpadding="2" cellspacing="0" width="100%">
218    <tr>
219      <td colspan="2" bgcolor="yellow"><b>Info</b></td>
220    </tr>
221    <tr>
222      <td colspan="2">
223        <table border="1" cellpadding="2" cellspacing="0" width="100%">
224          <tr>
225            <td valign="top" align="right"><b>resultFile</b></td>
226            <td bgcolor="cyan">'$resultFile'</td>
227          </tr>
228          <tr>
229            <td valign="top" align="right"><b>sql</b></td>
230            <td bgcolor="cyan">'$sql'</td>
231          </tr>
232          <tr>
233            <td valign="top" align="right"><b>plug</b></td>
234            <td bgcolor="cyan">'$plug'</td>
235          </tr>
236          <tr>
237            <td valign="top" align="right"><b>result</b></td>
238            <td bgcolor="cyan">'$self->{result}'</td>
239          </tr>
240          <tr>
241            <td valign="top" align="right"><b>error</b></td>
242            <td bgcolor="cyan">'$error'</td>
243          </tr>
244          <tr>
245            <td valign="top" align="right"><b>columns</b></td>
246            <td bgcolor="cyan">'$self->{columns}'</td>
247          </tr>
248          <tr>
249            <td valign="top" align="right"><b>numRows</b></td>
250            <td bgcolor="cyan">'$self->{numRows}'</td>
251          </tr>
252          <tr>
253            <td valign="top" align="right"><b>columnNames</b></td>
254            <td bgcolor="cyan">'$columnNames'</td>
255          </tr>
256        </table>
257      </td>
258    </tr>
259    <tr>
260      <td colspan="2" bgcolor="yellow"><b>rows</b></td>
261    </tr>
262    <tr>
263      <td colspan="2">
264        <table border="0" cellpadding="0" cellspacing="0" width="100%">
265          <tr>
266            <td width="25%">&nbsp;</td>
267            <td width="75%">
268              <table border="1" cellpadding="2" cellspacing="0" width="100%">
269END_OF_CODE
270      for (my $index=0; $index < $self->{numRows}; $index++)
271      {
272        $result .= <<"END_OF_CODE";
273                <tr>
274                  <td align="right" width="25%"><b><font color="red">$index</font></b></td>
275                  <td bgcolor="cyan" width="75%">&nbsp;</td>
276                </tr>
277                <tr>
278                  <td colspan="2">
279                    <table border="0" cellpadding="0" cellspacing="0" width="100%">
280                      <tr>
281                        <td width="25%">&nbsp;</td>
282                        <td width="75%">
283                          <table border="1" cellpadding="2" cellspacing="0" width="100%">
284END_OF_CODE
285        foreach my $column (@{$self->{columnNames}})
286        {
287          my $value = $self->encodeHTML(string => $self->{rows}->[$index]->{$column}, double => 1);
288          $result .= <<"END_OF_CODE";
289                            <tr>
290                              <td align="right"><b>$column</b></td>
291                              <td bgcolor="cyan">'$value'</td>
292                            </tr>
293END_OF_CODE
294        }
295        $result .= <<"END_OF_CODE";
296                          </table>
297                        </td>
298                      </tr>
299                    </table>
300                  </td>
301                </tr>
302END_OF_CODE
303      }
304      $result .= <<"END_OF_CODE";
305              </table>
306            </td>
307          </tr>
308        </table>
309      </td>
310    </tr>
311  </table>
312END_OF_CODE
313    }
314  }
315  else
316  {
317    if ($style eq "text")
318    {
319      $result .= "Config File not valid!\n\n";
320      $result .= join("\n", @{$valid[1]}) . "\n";
321    }
322    elsif ($style eq "html")
323    {
324      $result .= "Config File not valid!<br>\n<br>\n";
325      $result .= join("<br>\n", @{$valid[1]}) . "<br>\n";
326    }
327    die $result;
328  }
329
330  return $result;
331}
332
333sub encodeHTML
334{
335  my $self = shift;
336  my %args = ( string => "", double => 0, @_ );
337  my $string = $args{string};
338  my $double = $args{double};
339
340  $string =~ s/&/&amp;/g;
341  $string =~ s/</&lt;/g;
342  $string =~ s/>/&gt;/g;
343  if ($double)
344  {
345    $string =~ s/\\n/<br>\n/g;
346  }
347  else
348  {
349    $string =~ s/\n/<br>\n/g;
350  }
351  $string =~ s/  /&nbsp;&nbsp;/g;
352
353  return $string;
354}
355
356sub generateXML
357{
358  my $self = shift;
359  my $result = "";
360  my $errStr = "ResultSet->generateXML()  - Error:";
361
362  my @valid = $self->isValid();
363  if ($valid[0])
364  {
365    my $sql = $self->encodeEntities(string => $self->{sql});
366    my $plug = $self->encodeEntities(string => $self->{plug});
367    my $error = $self->encodeEntities(string => $self->{error});
368    $result .= <<"END_OF_XML";
369<?xml version="1.0" encoding="ISO-8859-1"?>
370<resultset version="$self->{version}">
371END_OF_XML
372    $result .= qq(  <select sql="$sql" plug="$plug"/>\n) if ($self->{select});
373    $result .= <<"END_OF_XML";
374  <status result="$self->{result}" error="$error"/>
375  <rows numRows="$self->{numRows}" columns="$self->{columns}">
376END_OF_XML
377    for (my $i=0; $i < $self->{numRows}; $i++)
378    {
379      if ($self->{columns})
380      {
381        $result .= "    <row>\n";
382        foreach my $column (@{$self->{columnNames}})
383        {
384          my $data = $self->{rows}->[$i]->{$column};
385          my $value = $self->encodeEntities(string => $data);
386          my $name = $self->fixupAttributes($column);
387          if ($self->{columns} == 1)
388          {
389            $result .= "      <column name=\"$name\" value=\"$value\"/>\n";
390          }
391          elsif ($self->{columns} == 2)
392          {
393            $result .= "      <$name><![CDATA[$data]]></$name>\n";
394          }
395        }
396        $result .= "    </row>\n";
397      }
398      else
399      {
400        $result .= "    <row";
401        foreach my $column (@{$self->{columnNames}})
402        {
403          my $value = $self->encodeEntities(string => $self->{rows}->[$i]->{$column});
404          my $name = $self->fixupAttributes($column);
405          $result .= " $name=\"$value\"";
406        }
407        $result .= "/>\n";
408      }
409    }
410    $result .= <<"END_OF_XML";
411  </rows>
412</resultset>
413END_OF_XML
414  }
415  else
416  {
417    $result .= "ResultSet not valid!\n\n";
418    $result .= join("\n", @{$valid[1]}) . "\n";
419    die $result;
420  }
421
422  return $result;
423}
424
425# string encodeEntities(string)
426# requires: string - string to encode
427# optional:
428# returns: string that has been encoded.
429# summary: replaces all special characters with their XML entity equivalent. " => &quot;
430sub encodeEntities
431{
432  my $self = shift;
433  my %args = ( string => "", @_ );
434  my $string = $args{string};
435
436  my @entities = ('&', '"', '<', '>', '\n');
437  my %entities = ('&' => '&amp;', '"' => '&quot;', '<' => '&lt;', '>' => '&gt;', '\n' => '\\n');
438
439  return $string if (length $string == 0);
440
441  foreach my $entity (@entities)
442  {
443    $string =~ s/$entity/$entities{$entity}/g;
444  }
445
446  return $string;
447}
448
449=item scalar fixupAttributes(string)
450
451Attempts to make sure that the given string can be a valid attribute in
452an xml document.
453
454Converts (, ), -, \, /, =, >, <, & to _
455Deletes ', ", \n
456
457=cut
458sub fixupAttributes
459{
460  my $self = shift;
461  my $string = "";
462  if (scalar @_ == 1)
463  {
464    $string = shift;
465  }
466  else
467  {
468    my %args = ( string => "", @_ );
469    $string = $args{string};
470  }
471
472  my @replace = ('&', '<', '>', '-', '\\(', '\\)', '\\\\', '/', '=');
473  my @delete = ("'", '"', '\n');
474
475  return $string if (length $string == 0);
476
477  foreach my $entity (@replace)
478  {
479    $string =~ s/$entity/_/g;
480  }
481
482  foreach my $entity (@delete)
483  {
484    $string =~ s/$entity//g;
485  }
486
487  return $string;
488}
489
4901;
491__END__
492
493=head1 NAME
494
495ResultSet - The XML Database ResultSet Module.
496
497=head1 SYNOPSIS
498
499  use DBIWrapper::ResultSet;
500  my $obj = DBIWrapper::ResultSet->new();
501
502=head1 DESCRIPTION
503
504ResultSet will contain the parsed XML file generated by readXML().
505It provides a method to validate that it is complete and also a method
506to generate a valid XML file from the data stored in the data hash.
507
508=head1 FUNCTIONS
509
510  scalar new()
511    Creates a new instance of the DBIWrapper::ResultSet
512    object.
513
514  array isValid(void)
515    Determines if the data structure is complete and usable for
516    generating an XML file from.
517    Returns an array.  The first index is boolean (1 or 0 to indicate
518    if the object is valid or not).  The second index is an array of
519    error messages that were generated based upon the errors found.
520
521  string displayData(style)
522    Outputs the contents of the perl data structure after validating
523    it.  If style = 'text' then formats for the console, else if
524    style = 'html' we generate valid html output and format in a table.
525    The content is suitable to place straight into the <body> section
526    of your HTML page.
527
528  string generateXML(void)
529    Creates an XML file based upon the info stored in the ResultSet.
530    It first calls isValid() to make sure this is possible.  If
531    not then we die with an informative error message.
532
533=head1 VARIABLES
534
535  version - version of the XML file parsed
536
537  resultFile - the name of the file parsed or the string of XML
538
539  sql - the SELECT statement issued which generated the XML
540
541  plug - the options that were plugged into the SQL statement
542
543  result - Ok or Error.  Indicates the status returned by the database
544
545  error - The error string returned if result = Error
546
547  numRows - the number of rows returned from the database
548
549  columns - 0 or 1.  Indicates if we had <column> children for each <row>
550            or if the data returned was stored by the name given from the
551            database in the <row> with no children tags.
552
553  rows - the array of row entries returned.  Each row is a hash of
554         column name = column value attributes.
555
556  columnNames - The names of the columns used in the XML file.
557
558  errorCodes - hash of error codes to messages returned by isValid().
559
560  NOTE:  All data fields are accessible by specifying the object
561         and pointing to the data member to be modified on the
562         left-hand side of the assignment.
563         Ex.  $obj->variable($newValue); or $value = $obj->variable;
564
565=head1 AUTHOR
566
567Xperience, Inc. (mailto:admin at pcxperience.com)
568
569=head1 SEE ALSO
570
571perl(1), DBIWrapper(3)
572
573=cut
574