# DBIWrapper.pm - The DataBase Wrapper Class that provides the DBI database
# connection and core functions for working with DBI databases.
# Created by James Pattie, 11/02/2000.

# Copyright (c) 2000-2002 Xperience, Inc. http://www.pcxperience.com/
# All rights reserved.  This program is free software; you can redistribute it
# and/or modify it under the same terms as Perl itself.

package DBIWrapper;
use strict;
use DBI;
use vars qw($AUTOLOAD $VERSION @ISA @EXPORT @EXPORT_OK);

require Exporter;

@ISA = qw(Exporter AutoLoader);
@EXPORT = qw();

$VERSION = '0.20';

=head1 NAME

DBIWrapper - Perl extension for generic DBI database access.

=head1 SYNOPSIS

  use DBIWrapper;
  my $db = DBIWrapper->new(dbType => "Pg",
                           dbName => "test_db",
                           dbHost => "localhost",
                           dbUser => "nobody",
                           dbPasswd => "",
                           dbPort => "5432",
                           predefinedDSN => "",
                           printError => 1,
                           raiseError => 1,
                           autoCommit => 0);
  if ($db->didErrorOccur())
  {
    die $db->errorMessage();
  }

  $db->read("SELECT * FROM test_tb");
  $db->write(sql => "INSERT INTO test_tb (name, value) VALUES (?, ?)",
  		plug => [ $name, $value ]);
  # this used DBI's substitution features to plugin the name and value.

  $db->close();  # close down the database connection.  Any read's
  # or write's will no longer be valid with this object until a new is
  # issued again.

=head1 DESCRIPTION

DBIWrapper is the generic database Object for accessing the DBI database
interface.  It provides the lowest level of functionality needed by any
program wanting to access databases via the DBI.  Currently, DBIWrapper
is only aware of Pg (PostgreSQL), mysql (MySQL) and ODBC DBD modules and
how to work with them correctly.

Support for transactions on MySQL is now checked for and if found to be
available, the AutoCommit flag is turned off so that transactions will
be used.

The substitution array (if used) will cause each ##?1##, ##?2##, etc.
string in the sql string to be substituted for the corresponding value
in the substitution array.  It must start at ?1.  It is up to the user
to pass in the correct number of elements for both the plug and
substitution arrays.  The plug array is used to pass in the values for
DBI to replace in the sql string of ? which is standard DBI notation.

=head1 Exported FUNCTIONS

B<NOTE>: I<bool> = 1(true), 0(false)

=over 4

=item scalar new(dbType, dbName, dbHost, dbUser, dbPasswd, dbPort, printError, raiseError, autoCommit, predefinedDSN, setDateStyle, logLevel)

 Creates a new instance of the DBIWrapper object and opens
 a connection to the specified database.  If predefinedDSN is
 specified then it is used instead of the dbName, dbHost, dbPort
 values.  This is mainly to support ODBC easier.
 If setDateStyle is 1 (default) and dbType = Pg, then the datestyle
 for PostgreSQL is set to US (MM/DD/YYYY).
 logLevel defaults to 0.  There are 4 levels 0, 1, 2 and 3 which log
 the following items when an error occurs:
 0) Nothing is output
 1) dbType, dbHost, dbName, printError, raiseError, autoCommit,
   setDateStyle, supportsTransactions, transactionType
 2) all of 1 plus dbUser, dbPort, predefinedDSN
 3) all of 2 plus dbPasswd

=cut
sub new
{
  my $that = shift;
  my $class = ref($that) || $that;
  my $self = bless {}, $class;
  my %args = ( dbType => 'Pg', dbHost => 'localhost', dbUser => 'nobody', dbPasswd => '', dbPort => '5432', predefinedDSN => "", printError => 1, raiseError => 1, autoCommit => 0, setDateStyle => 1, logLevel => 0, @_ );
  my ($dbType, $dbName, $dbHost, $dbUser, $dbPasswd, $dbPort, $predefinedDSN, $printError, $raiseError, $autoCommit, $setDateStyle, $logLevel);
  my $errStr = "DBIWrapper::new - ";

  $dbType = $args{dbType};
  $dbHost = $args{dbHost};
  $dbName = $args{dbName};
  $dbUser = $args{dbUser};
  $dbPasswd = $args{dbPasswd};
  $dbPort = $args{dbPort};
  $predefinedDSN = $args{predefinedDSN};
  $printError = $args{printError};
  $raiseError = $args{raiseError};
  $autoCommit = $args{autoCommit};
  $setDateStyle = $args{setDateStyle};
  $logLevel = $args{logLevel};
  $self->{supportsTransactions} = 1;  # by default all Databases support Transactions, except for MySQL.
  $self->{transactionType} = "";  # This is only set by MySQL so we know what type of transaction support is available.
  if ($dbType eq "mysql")
  {
    $autoCommit = 1;  # it may not do transactions yet.
    $self->{supportsTransactions} = 0;
  }

  $self->{error} = 0;  # nothing wrong yet.
  $self->{errorString} = "";

  $self->{dbType} = $dbType;
  $self->{dbHost} = $dbHost;
  $self->{dbName} = $dbName;
  $self->{dbUser} = $dbUser;
  $self->{dbPasswd} = $dbPasswd;
  $self->{dbPort} = $dbPort;
  $self->{predefinedDSN} = $predefinedDSN;
  $self->{printError} = $printError;
  $self->{raiseError} = $raiseError;
  $self->{autoCommit} = $autoCommit;
  $self->{setDateStyle} = $setDateStyle;
  $self->{logLevel} = $logLevel;
  $self->{dbh} = undef;  # set this explicitly now so that we have something to check if an error occurs later.

  if (!$self->isValid(new => 1))
  {
    $self->setError(errorString => $errStr . "Error!<br>\n" . $self->errorMessage);
    return $self;
  }

  my $dbh;
  my $dsn;
  $dsn = "dbi:$dbType:";

  if ($dbType eq "Pg" || $dbType eq "mysql")
  {
    if (length $predefinedDSN > 0)
    {
      $dsn .= $predefinedDSN;
    }
    else
    {
      $dsn .= "dbname=$dbName;host=$dbHost;port=$dbPort";
    }
  }
  elsif ($dbType eq "ODBC")
  {
    if (length $predefinedDSN > 0)
    {
      $dsn .= $predefinedDSN;
    }
    else
    {
      $self->setError(errorString => $errStr . "Error!<br>\nYou must specify the 'predefinedDSN' for dbType = '$dbType'!<br>\n" . $self->debugMessage);
      return $self;
    }
  }

  eval {
    $dbh = DBI->connect($dsn, $dbUser, $dbPasswd, { PrintError => $printError, RaiseError => $raiseError, AutoCommit => $autoCommit });
  };
  if ($@)
  {
    $self->{dbh} = undef;
    $self->setError(errorString => $errStr . "Eval of connect failed!<br>\nError = '$@'.<br>\nDBIError = '" . $DBI::errstr . "'.<br>\n" . $self->debugMessage);
  }
  else
  {
    if ($dbh)
    {
      if (!$DBI::err)
      {
        $self->{dbh} = $dbh;
        if ($dbType eq "Pg" && $setDateStyle)
        {
          $self->write(sql => "SET datestyle TO 'POSTGRES,US'");
        }
        if ($dbType eq "mysql")
        {
          # check for Transaction support, and if present disable the AutoCommit flag.
          if ($self->mysqlHasTransactions)
          {
            $self->{supportsTransactions} = 1;
            $self->{dbh}->{AutoCommit} = 0;
          }
          if ($self->didErrorOccur)
          {
            $self->{dbh} = undef;
            $self->setError(errorString => $errStr . "Checking for Transactions with MySQL failed!<br>\n" . $DBI::errstr . "<br>\n" . $self->debugMessage);
          }
        }
      }
      else
      {
        $self->{dbh} = undef;
        $self->setError(errorString => $errStr . "connect failed!<br>\n" . $DBI::errstr . "<br>\n" . $self->debugMessage);
      }
    }
    else
    {
      $self->{dbh} = undef;
      $self->setError(errorString => $errStr . "connect failed!<br>\n" . $DBI::errstr . "<br>\n" . $self->debugMessage);
    }
  }

  return $self;
}

=item bool isValid()

 Returns 1 if the DBI object is valid, else 0 if invalid.

=cut
sub isValid
{
  my $self = shift;
  my %args = ( new => 0, @_ );
  my $new = $args{new};
  my $valid = 1;
  my $errorString = "";

  if (!$new)
  {
    if (!defined $self->{dbh})
    {
      $errorString .= "dbh is not defined!<br>\n";
      $valid = 0;
    }
  }
  if ($self->{dbType} !~ /^(Pg|mysql|ODBC)$/)
  {
    $errorString .= "dbType = '$self->{dbType}' is invalid!<br>\n";
    $valid = 0;
  }
  if (length $self->{predefinedDSN} == 0) # only check the dbHost, dbName and dbPort values if not using predefinedDSN
  {
    if (length $self->{dbHost} == 0)
    {
      $errorString .= "dbHost = '$self->{dbHost}' is invalid!<br>\n";
      $valid = 0;
    }
    if (length $self->{dbName} == 0)
    {
      $errorString .= "dbName = '$self->{dbName}' is invalid!<br>\n";
      $valid = 0;
    }
    if ($self->{dbPort} !~ /^(\d+)$/)
    {
      $errorString .= "dbPort = '$self->{dbPort}' is invalid!<br>\n";
      $valid = 0;
    }
  }
  if (length $self->{dbUser} == 0)
  {
    $errorString .= "dbUser = '$self->{dbUser}' is invalid!<br>\n";
    $valid = 0;
  }
  if (length $self->{dbPasswd} == 0)
  {
    $errorString .= "dbPasswd = '$self->{dbPasswd}' is invalid!<br>\n";
    $valid = 0;
  }
  if ($self->{autoCommit} !~ /^(0|1)$/)
  {
    $errorString .= "autoCommit = '$self->{autoCommit}' is invalid!<br>\n";
    $valid = 0;
  }
  if ($self->{printError} !~ /^(0|1)$/)
  {
    $errorString .= "printError = '$self->{printError}' is invalid!<br>\n";
    $valid = 0;
  }
  if ($self->{raiseError} !~ /^(0|1)$/)
  {
    $errorString .= "raiseError = '$self->{raiseError}' is invalid!<br>\n";
    $valid = 0;
  }
  if ($self->{setDateStyle} !~ /^(0|1)$/)
  {
    $errorString .= "setDateStyle = '$self->{setDateStyle}' is invalid!<br>\n";
    $valid = 0;
  }
  if ($self->{logLevel} !~ /^(0|1|2|3)$/)
  {
    $errorString .= "logLevel = '$self->{logLevel}' is invalid!<br>\n";
    $valid = 0;
  }

  if (!$valid)
  {
    $self->setError(errorString => "DBIWrapper::isValid  - Error!<br>\n$errorString" . $self->debugMessage);
  }

  return $valid;
}

=item void close()

  Closes the connection to the database.

=cut
sub close
{
  my $self = shift;
  my $errStr = "DBIWrapper::close - ";

  if (defined $self->{dbh})
  {
    my $result;
    eval { $result = $self->{dbh}->disconnect; };
    if ($@)
    {
      $self->setError(errorString => $errStr . "Eval of disconnect failed!<br>\nError = '$@'.<br>\n" . $self->debugMessage);
      return;
    }
    else
    {
      eval {
        if (!$result || $self->{dbh}->err)
        {
          $self->setError(errorString => $errStr . "disconnect failed!<br>\n" . $self->{dbh}->errstr . "<br>\n" . $self->debugMessage);
          return;
        }
      };
      if ($@)
      {
        $self->setError(errorString => $errStr . "Eval of result check failed!<br>\nError = '$@'.<br>\n" . $self->debugMessage);
        return;
      }
    }
    $self->{dbh} = undef;  # signal it is no longer valid!
  }
}

=item void setError(errorString)

 sets $error = 1 and $errorString = errorString

=cut
sub setError
{
  my $self = shift;
  my $errorString = "";
  if (scalar @_ == 1)
  {
    $errorString = shift;
  }
  else
  {
    my %args = ( errorString => "", @_ );
    $errorString = $args{errorString};
  }

  $self->{errorString} = $errorString;
  $self->{error} = 1;
}

=item bool didErrorOccur()

 returns the value of error.

=cut
sub didErrorOccur
{
  my $self = shift;

  return $self->{error};
}

=item scalar errorMessage()

 returns the value of errorString.

=cut
sub errorMessage
{
  my $self = shift;

  return $self->{errorString};
}

=item void commit()

 causes the database to commit the current transaction.  Only works
 if AutoCommit is set to 0 and the database supports Transactions.

=cut
sub commit
{
  my $self = shift;
  my $errStr = "DBIWrapper::commit - ";

  if (!$self->{supportsTransactions})
  {
    return;
  }
  eval { $self->{dbh}->commit; };
  if ($@)
  {
    $self->setError(errorString => $errStr . "commit failed!<br>\nError = $@" . "<br>\n" . $self->debugMessage);
  }
  elsif ($DBI::err)
  {
    $self->setError(errorString => $errStr . "commit failed!<br>\nError = $DBI::errstr" . "<br>\n" . $self->debugMessage);
  }
}

=item void rollback()

 causes the database to rollback the current transaction.  Only
 works if AutoCommit is set to 0 and the database supports
 Transactions.

=cut
sub rollback
{
  my $self = shift;
  my $errStr = "DBIWrapper::rollback - ";

  if (!$self->{supportsTransactions})
  {
    return;
  }
  eval { $self->{dbh}->rollback; };
  if ($@)
  {
    $self->setError(errorString => $errStr . "rollback failed!<br>\nError = $@" . "<br>\n" . $self->debugMessage);
  }
  elsif ($DBI::err)
  {
    $self->setError(errorString => $errStr . "rollback failed!<br>\nError = $DBI::errstr" . "<br>\n" . $self->debugMessage);
  }
}

=item ref read(sql => "", plug => [], substitute => [])

 (This function should only be called for SELECT statements).
 executes the specified sql statement passing in any values in plug
 to the execute method after doing any substitutions that are in
 substitute.  The resulting sql data is passed back to the user as a
 reference for them to do with as they please.

=cut
sub read
{
  my $self = shift;
  my $sql = "";
  my @plug = ();
  my @substitute = ();
  my $errStr = "DBIWrapper::read - ";
  if (scalar @_ == 1)
  {
    $sql = shift;
  }
  else
  {
    my %args = ( plug => [], substitute => [], @_ );
    @plug = @{$args{'plug'}};
    @substitute = @{$args{'substitute'}};

    $sql = $args{'sql'};
  }
  # validate we got a sql statement to work with.
  if (length $sql == 0)
  {
    $self->setError(errorString => $errStr . "SQL string not passed in!" . "<br>\n" . $self->debugMessage);

    return undef;
  }

  # check and see if we need to do any substitutions.
  if (scalar @substitute > 0)
  {
    for (my $i=0; $i < scalar @substitute; $i++)
    {
      my $temp_string = "\\#\\#\\?" . ($i+1) . "\\#\\#";
      $sql =~ s/$temp_string/$substitute[$i]/g;
    }
  }

  # now prepare the statement
  my $sth;
  eval {
    $sth = $self->{dbh}->prepare($sql);
  };
  if ($@)
  {
    $self->setError(errorString => $errStr . "Eval of prepare failed!<br>\nError = '$@'.<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return undef;
  }
  elsif (!$sth || $DBI::err)
  {
    $self->setError(errorString => $errStr . "Preparing failed!<br>\n" . $DBI::errstr . "<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return undef;
  }

  # now execute the sql statement passing in any parameters given via plug
  my $rc;
  eval {
    $rc = $sth->execute(@plug);
  };
  if ($@)
  {
    $self->setError(errorString => $errStr . "Eval of execute failed!<br>\nError = '$@'.<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return undef;
  }
  elsif (!$rc || $DBI::err)
  {
    $self->setError(errorString => $errStr . "Execute failed!<br>\n" . $DBI::errstr . "<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return undef;
  }

  return $sth;
}

=item scalar readXML(sql, plug, substitute, columns)

 requires: sql
 optional: plug, substitute, columns = 0
 returns:  valid XML document describing the data selected from the
  database.  Calls read() to actually validate the data and
  execute the SELECT statement.  The resulting XML document
  will either have an error condition set (if read() signaled
  an error occured) or will be the result of traversing the
  sth object returned from read().

  If columns = 0, then all info will be returned in the <row>
  tag as attributes where the column name = column value.
  Ex.  <row name="test" value="testing" other="something else"/>
  When the column names were name, value and other.

  If columns = 1, then all info will be returned in <column>
  tags which are children of the <row> tag.  A column tag has
  attributes name and value.  name = column name and value =
  column value.
  Ex.
  <row>
    <column name="name" value="test"/>
    <column name="value" value="testing"/>
  </row>

  The XML format is as follows:
  <?xml version="1.0" encoding="ISO-8859-1"?>
  <resultset version="1.1">
    <select sql="" plug=""/>
    <status result="" error=""/>
    <rows numRows="" columns="0|1">
      <row/>
    </rows>
  </resultset>

  If the XML document is an error document, then:
  <status result="Error" error="Error message"/>
  else
  <status result="Ok" error=""/>

  In <select> tag, sql = The sql SELECT string, plug = the
  string made when joining all the plug array entries
  together and comma seperating them.  The entries are
  single quoted.  Ex. plug="''" would represent no plug
  entries used.  plug="'x', 'y'" would mean that 2 entries
  were passed in: x, y.

  In <rows> numRows will be equal to the number of rows
  being returned or 0 if an error had occured.

  The <row> tag will depend on the value of columns.

=cut
sub readXML
{
  my $self = shift;
  my $sql = "";
  my @plug = ();
  my $plug = "";
  my $columns = 0;
  my $sth = undef;
  if (scalar @_ == 1)
  {
    $sql = shift;
    $sth = $self->read($sql);
  }
  else
  {
    my %args = ( plug => [], substitute => [], columns => 0, @_ );
    @plug = @{$args{plug}};
    $plug = "'" . join("', '", @plug) . "'";
    $sql = $args{sql};
    $columns = $args{columns};
    $sth = $self->read(%args);
  }
  my $errStr = "DBIWrapper::readXML - ";
  my $xmlDoc = "";

  my $resultSetVersion = "1.1";  # Update whenever the xml format changes.

  # make sure we don't have any invalid XML characters in the sql or plug strings.
  $sql = $self->encodeEntities(string => $sql);
  $plug = $self->encodeEntities(string => $plug);

  if ($self->didErrorOccur)
  {
    $self->setError(errorString => $errStr . $self->errorMessage);
    $self->{error} = 0;  # turn off the error message as the XML file will convey it.
    my $errorString = $self->encodeEntities(string => $self->errorString);

    $xmlDoc = <<"END_OF_CODE";
<?xml version="1.0" encoding="ISO-8859-1"?>
<resultset version="$resultSetVersion">
  <select sql="$sql" plug="$plug"/>
  <status result="Error" error="$errorString"/>
  <rows numRows="0" columns="$columns"/>
</resultset>
END_OF_CODE
  }
  else
  { # now process the result set returned and generate the XML document.
    $xmlDoc = <<"END_OF_CODE";
<?xml version="1.0" encoding="ISO-8859-1"?>
<resultset version="$resultSetVersion">
  <select sql="$sql" plug="$plug"/>
  <status result="Ok" error=""/>
  <rows numRows="#NUMROWS#" columns="$columns">
END_OF_CODE
    my $names = $sth->{NAME_lc};  # get list of the names of the columns returned. (Lowercased)

    # fixup the names to make sure they don't have any invalid XML characters in them.
    foreach my $name (@{$names})
    {
      $name = $self->encodeEntities(string => $name);
    }

    while (my $row = $sth->fetchrow_arrayref)
    {
      if ($columns)
      {
        # start the <row>
        $xmlDoc .= "    <row>\n";

        # now iterate over each entry in the row and create it's column tag.
        for(my $i=0; $i < scalar @{$row}; $i++)
        {
          my $data = $row->[$i];
          $data = "NULL" if (not defined $data);  # set NULL entries to be NULL
          $data = $self->encodeEntities(string => $data);

          $xmlDoc .= "      <column name=\"$names->[$i]\" value=\"$data\"/>\n";
        }

        # end the <row>
        $xmlDoc .= "    </row>\n";
      }
      else
      {
        # start the <row>
        $xmlDoc .= "    <row";

        for(my $i=0; $i < scalar @{$row}; $i++)
        {
          my $data = $row->[$i];
          $data = "NULL" if (not defined $data);  # set NULL entries to be NULL
          $data = $self->encodeEntities(string => $data);

          $xmlDoc .= " $names->[$i]=\"$data\"";
        }

        $xmlDoc .= "/>\n"; # end the <row>
      }
    }
    $xmlDoc .= <<"END_OF_CODE";
  </rows>
</resultset>
END_OF_CODE

    # now update the numRows value.
    my $numRows = $sth->rows;
    $xmlDoc =~ s/#NUMROWS#/$numRows/;
  }

  return $xmlDoc;
}

=item string encodeEntities(string)

 requires: string - string to encode
 optional:
 returns: string that has been encoded.
 summary: replaces all special characters with their XML entity
   equivalent. " => &quot;

=cut
sub encodeEntities
{
  my $self = shift;
  my $string = "";
  if (scalar @_ == 1)
  {
    $string = shift;
  }
  else
  {
    my %args = ( string => "", @_ );
    $string = $args{string};
  }

  my @entities = ('&', '"', '<', '>', '\n');
  my %entities = ('&' => '&amp;', '"' => '&quot;', '<' => '&lt;', '>' => '&gt;', '\n' => '\\n');

  return $string if (length $string == 0);

  foreach my $entity (@entities)
  {
    $string =~ s/$entity/$entities{$entity}/g;
  }

  return $string;
}

=item scalar readHTML(sql, plug, substitute, tableClass, alternateRows,
displayNumRows)

 requires: sql
 optional: plug, substitute, tableClass, alternateRows, 
  displayNumRows
 returns:  valid HTML <table> describing the data selected from the
  database.  Calls read() to actually validate the data and
  execute the SELECT statement.  The resulting HTML <table>
  will either have the error displayed (if read() signaled
  an error occured) or will be the result of traversing the
  sth object returned from read().
  
 If an error occured, then the generated tr and td will have
 class="sqlError" assigned to them so you can change the way the
 sql Error row is displayed.  The error output will also be
 wrapped in a <span class="sqlError"></span> so you can change
 the display behaviour.
  
 tableClass defines the class to assign to this table so it knows
 how to display itself.  Defaults to "".  This allows you to have
 different readHTML generated tables on the same page and let them
 look different (border, width, cellspacing, cellpadding, etc.).
 
 alternateRows (boolean) lets the caller indicate they want to 
 possibly have different row characteristics on every other row.  
 Defaults to 1.
 
 displayNumRows (boolean) lets the caller indicate they want a <p>
 above the generated table that tells the user how many rows were
 returned.  Defaults to 1.  The generated paragraph has 
 class="sqlNumRows" assigned to it so the caller can affect the
 characteristics of the output and the NumRows statement is wrapped 
 in a <span class="sqlNumRows"></span>.
 
 The table header will be made up from the returned columns in the
 sql statement.  Each <th> will have the css class="column_name"
 defined so that the callers style can have th.column_name defined 
 to dictate how the <th> is to be displayed.  The <tr> for the table 
 header will have class="sqlHeader" assigned to it.
 
 Each <tr> will have class="sqlRow" assigned, unless alternateRows
 is enabled, which then causes the even rows to have class="sqlRow"
 and the odd rows to have class="sqlRow2" assigned. Each <td> will 
 have the css class="column_name" defined so the callers style
 can have td.column_name defined to dictate how the <td> is to be
 displayed. The contents of the <td> entry will be wrapped in
 <span class="column_name"></span> to allow even more display 
 control.

=cut
sub readHTML
{
  my $self = shift;
  my $sql = "";
  my @plug = ();
  my $plug = "";
  my $tableClass = "";
  my $alternateRows = 1;
  my $displayNumRows = 1;
  my $sth = undef;
  if (scalar @_ == 1)
  {
    $sql = shift;
    $sth = $self->read($sql);
  }
  else
  {
    my %args = ( plug => [], substitute => [], tableClass => "", alternateRows => 1, displayNumRows => 1, @_ );
    @plug = @{$args{plug}};
    $plug = "'" . join("', '", @plug) . "'";
    $sql = $args{sql};
    $tableClass = $args{tableClass};
    $alternateRows = $args{alternateRows};
    $displayNumRows = $args{displayNumRows};
    $sth = $self->read(%args);
  }
  my $errStr = "DBIWrapper::readHTML - ";
  my $html = "";
  
  # make sure the defaults are sane.  The error handling will drop through and be caught so we output the table, etc.
  if ($alternateRows !~ /^(0|1)$/)
  {
    $self->setError($self->errorMessage . $errStr . "alternateRows = '$alternateRows' is invalid!");
  }
  if ($displayNumRows !~ /^(0|1)$/)
  {
    $self->setError($self->errorMessage . $errStr . "displayNumRows = '$displayNumRows' is invalid!");
  }

  if ($self->didErrorOccur)
  {
    $self->setError(errorString => $errStr . $self->errorMessage);
    $self->{error} = 0;  # turn off the error message as the HTML Table will convey it.
    my $errorString = $self->errorMessage;

    if ($displayNumRows)
    {
      $html .= <<"END_OF_CODE";
<p class="sqlNumRows"><span class="sqlNumRows"><b>0</b> rows returned.</span></p>
END_OF_CODE
    }
    $html .= <<"END_OF_CODE";
<table class="$tableClass">
  <tr class="sqlError">
    <td class="sqlError"><span class="sqlError">$errorString</span></td>
  </tr>
</table>
END_OF_CODE
  }
  else
  { # now process the result set returned and generate the HTML Table.
    if ($displayNumRows)
    {
      $html .= <<"END_OF_CODE";
<p class="sqlNumRows"><span class="sqlNumRows"><b>#NUMROWS#</b> rows returned.</span></p>
END_OF_CODE
    }
    $html .= "<table class=\"$tableClass\">\n";
    my $names = $sth->{NAME_lc};  # get list of the names of the columns returned. (Lowercased)

    # display the headers
    $html .= "  <tr class=\"sqlHeader\">\n";
    foreach my $name (@{$names})
    {
      $html .= "    <th class=\"$name\">$name</th>\n";
    }
    $html .= "  </tr>\n";

    my $counter = 0;
    while (my $row = $sth->fetchrow_arrayref)
    {
      my $class = ($alternateRows ? ($counter % 2 == 0 ? "sqlRows" : "sqlRows2") : "sqlRows");
      $html .= "  <tr class=\"$class\">\n";
      # now iterate over each entry in the row and create it's column tag.
      for (my $i=0; $i < scalar @{$row}; $i++)
      {
        my $data = $row->[$i];
        $data = "NULL" if (not defined $data);  # set NULL entries to be NULL

        $html .= "    <td class=\"" . $names->[$i] . "\"><span class=\"" . $names->[$i] . "\">$data</span></td>\n";
      }

      # end the <row>
      $html .= "  </tr>\n";
      $counter++;
    }
    $html .= "</table>\n";

    # now update the numRows value.
    my $numRows = $sth->rows;
    $html =~ s/#NUMROWS#/$numRows/;
  }

  return $html;
}

=item int write(sql => "", plug => [], substitute => [])

 (This function should be called for any sql statement other than
 SELECT).
 executes the specified sql statement passing in any values in plug
 to the execute method after doing any substitutions that are in
 substitute.

 Returns the number of rows affected.

=cut
sub write
{
  my $self = shift;
  my $sql = "";
  my @plug = ();
  my @substitute = ();
  if (scalar @_ == 1)
  {
    $sql = shift;
  }
  else
  {
    my %args = ( plug => [], substitute => [], @_ );
    @plug = @{$args{'plug'}};
    @substitute = @{$args{'substitute'}};
    $sql = $args{'sql'};
  }
  my $errStr = "DBIWrapper::write - ";

  # validate we got a sql statement to work with.
  if (length $sql == 0)
  {
    $self->setError(errorString => $errStr . "SQL string not passed in!" . "<br>\n" . $self->debugMessage);

    return 0;
  }

  # check and see if we need to do any substitutions.
  if (scalar @substitute > 0)
  {
    for (my $i=0; $i < scalar @substitute; $i++)
    {
      my $temp_string = "\\#\\#\\?" . ($i+1) . "\\#\\#";
      $sql =~ s/$temp_string/$substitute[$i]/g;
    }
  }

  # now prepare the statement
  my $sth;
  eval {
    $sth = $self->{dbh}->prepare($sql);
  };
  if ($@)
  {
    $self->setError(errorString => $errStr . "Eval of prepare failed!<br>\nError = '$@'.<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return 0;
  }
  elsif (!$sth || $DBI::err)
  {
    $self->setError(errorString => $errStr . "Preparing failed!<br>\n" . $DBI::errstr . "<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return 0;
  }

  # now execute the sql statement passing in any parameters given via plug
  my $rv;
  eval {
    $rv = $sth->execute(@plug);
  };
  if ($@)
  {
    $self->setError(errorString => $errStr . "Eval of execute failed!<br>\nError = '$@'.<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return 0;
  }
  elsif (!$rv || $DBI::err)
  {
    $self->setError(errorString => $errStr . "Execute failed!<br>\n" . $DBI::errstr . "<br>\nsql='$sql'.<br>\nplug='" . join("', '", @plug) . "'.<br>\n" . $self->debugMessage);

    return 0;
  }

  return $rv;
}

# This routine checks to see if the mysql database (currently connected) supports transactions
# and sets the transaction type variable.
sub mysqlHasTransactions
{
  my $self = shift;
  my $errStr = "DBIWrapper::mysqlHasTransactions - ";
  return 0 if (!defined $self->{dbh});

  return 1 if ($self->{dbType} ne "mysql");  # make sure we are working with a MySQL database.
  my $sth = $self->{dbh}->prepare("SHOW VARIABLES");
  my $rv;
  eval { $rv = $sth->execute(); };
  if ($@ || $DBI::err)
  {
    $self->setError(errorString => $errStr . "Execute failed!<br>\n" . $DBI::errstr . "<br>\n" . $self->debugMessage);
    return 0;
  }
  while (my $row = $sth->fetchrow_hashref())
  {
    if ($row->{Variable_name} eq 'have_bdb' && $row->{Value} eq 'YES')
    {
      $self->{transactionType} = "bdb";
      last;
    }
    if ($row->{Variable_name} eq 'have_innobase' && $row->{Value} eq 'YES')
    {
      $self->{transactionType} = "innobase";
      last;
    }
    if ($row->{Variable_name} eq 'have_gemini' && $row->{Value} eq 'YES')
    {
      $self->{transactionType} = "gemini";
      last;
    }
  }
  return $self->{transactionType};
}

=item string debugMessage()

 Returns the string that contains all the info that is to be logged
 at the current logLevel level.  If logLevel is not 0, 1, 2 or 3 
 then a default of 3 is used.

=cut
sub debugMessage
{
  my $self = shift;
  my $logLevel = $self->{logLevel};
  my $result = "";
  $logLevel = 3 if ($logLevel !~ /^(0|1|2|3)$/);

  return $result if ($logLevel == 0);  # jump out now since they don't want any debug info displayed.

  # output the stuff that will always be done (level = 1)
  $result .= "dbType = '$self->{dbType}', dbHost = '$self->{dbHost}', dbName = '$self->{dbName}', printError = '$self->{printError}', raiseError = '$self->{raiseError}'";
  $result .= ", autoCommit = '$self->{autoCommit}', setDateStyle = '$self->{setDateStyle}', supportsTransactions = '$self->{supportsTransactions}', transactionType = '$self->{transactionType}'";

  # output level 2 stuff
  if ($logLevel >= 2)
  {
    $result .= ", dbUser = '$self->{dbUser}', dbPort = '$self->{dbPort}', predefinedDSN = '$self->{predefinedDSN}'";
  }

  # output level 3 stuff
  if ($logLevel == 3)
  {
    $result .= ", dbPasswd = '$self->{dbPasswd}'";
  }

  $result .= "<br>\n";  # tack on the newline ending

  return $result;
}

=item int getLogLevel()

 returns the current logLevel value.

=cut
sub getLogLevel
{
  my $self = shift;

  return $self->{logLevel};
}

=item int setLogLevel(logLevel => 1)

 sets the logLevel value.  If the value is not specified then it
 defaults to logLevel 1.

 Returns 1 on Success, 0 on Error.

 We validate that the logLevel is 1, 2 or 3.

=cut
sub setLogLevel
{
  my $self = shift;
  my $logLevel = 1;
  if (scalar @_ == 1)
  {
    $logLevel = shift;
  }
  else
  {
    my %args = ( logLevel => 1, @_ );
    $logLevel = $args{logLevel};
  }
  my $errStr = "DBIWrapper::setLogLevel - ";

  if ($logLevel !~ /^(0|1|2|3)$/)
  {
    $self->setError(errorString => $errStr . "logLevel = '$logLevel' is invalid!<br>\n" . $self->debugMessage);
    return 0;
  }

  $self->{logLevel} = $logLevel;
  return 1;
}

=item string boolToDBI(string)

 Takes the string and returns a 1 for 1|t|true,
 returns a 0 for anything else.

 This method basically will output a true or false
 value that any database should recognize based upon
 the input string.

=cut
sub boolToDBI
{
  my $self = shift;
  my $string = "";
  if (scalar @_ == 1)
  {
    $string = shift;
  }
  else
  {
    my %args = ( string => "", @_ );
    $string = $args{string};
  }
  my $errStr = "DBIWrapper::boolToDBI - ";

  if ($string =~ /^(1|t|true)$/i)
  {
    return 1;
  }
  # must be false!
  return 0;
}

=item string dbiToBool(string)

 Takes the 1 or 0 from the DBI and returns
 true or false.

=cut
sub dbiToBool
{
  my $self = shift;
  my $string = "";
  if (scalar @_ == 1)
  {
    $string = shift;
  }
  else
  {
    my %args = ( string => "", @_ );
    $string = $args{string};
  }
  my $errStr = "DBIWrapper::dbiToBool - ";

  if ($string =~ /^(1)$/i)
  {
    return "true";
  }
  # must be false!
  return "false";
}

sub AUTOLOAD  # Read only access to data objects.
{
  my $self = shift;
  my $type = ref($self) || die "$self is not an object";
  my $name = $AUTOLOAD;
  $name =~ s/.*://;	# strip fully-qualified portion
  unless (exists $self->{$name})
  {
    die "Can't access `$name' field in object of class $type";
  }
  return $self->{$name};
}

sub DESTROY
{
  my $self = shift;

  $self->close;
  if ($self->didErrorOccur)
  {
    die "DBIWrapper->DESTROY: " . $self->errorMessage;
  }
}

=back

=cut

1;
__END__

=head1 NOTE:

 All data fields are accessible by specifying the object and
 variable as follows:
 Ex.  $value = $obj->variable;

 Any methods where it is possible to specify just a single
 argument and still have it be valid, you can now specify
 the argument without having to name it first.

 Ex:  calling read() without using the substitute or plug
 options can be done as $dbi->read("SELECT * from test");

 Methods updated to support this:
 setError, read, readXML, write, setLogLevel

=head1 AUTHOR

James A. Pattie, james at pcxperience dot com

=head1 SEE ALSO

perl(1), DBI(3), DBIWrapper::XMLParser(3), DBIWrapper::ResultSet(3)

=cut
