# -----------------------------------------------------------------------------
# TWC
# Treuwert-Computer GmbH
# Schloßbergring 9
# 79098 Freiburg
#
# Author: Klaus Reger <K.Reger@twc.de>
#
# -----------------------------------------------------------------------------
# Program-Data
# -----------------------------------------------------------------------------
# Project  :
# System   :
# Program  :
# Module   : $RCSfile: DbSession.pm,v $
# Version  : $Revision: 1.6 $
# Date     : $Date: 2002/03/15 09:41:42 $
# State    : $State: Exp $
#
# Description:
#   This module handles all the things of WWWdb, which have
#   to do with DbSessions
#
# -----------------------------------------------------------------------------
# $Log: DbSession.pm,v $
# Revision 1.6  2002/03/15 09:41:42  k_reger
# - Map Datatypes to DBI-standard
# - Date-conversion fixes
#
# Revision 1.5  2002/02/28 15:32:30  k_reger
# - include DB-Plugins with require
#
# Change-log 
#
# --- Known Bugs -------------
# please look at the BUGS-File
# --- TODO -------------------
# please look at the TODO-File
# ----------------------------------------------------------------------------
use DBI;
use Data::Dumper;
use POSIX qw(strftime);

use strict;

=head1 NAME

WWWdb::DbSession - DbSessions for WWWdb

=head1 SYNOPSIS

 use WWWdb::DbSession;

 $oDbSession     = DbSession->new($cDbDriver,
                                  $cDbName,
                                  $cUserName,
                                  $cPassword,
                                  [RaiseError => $],
                                  [PrintError => $],
                                  [AutoCommit => $]);

 $oDbSession->SqlDo($cSQL_Statement,
                    [@lBindValues]);

 $lResult = $oDbSession->SqlSelect($cSQL_Statement,
                                   [@lBindValues]);

 $iMaxId = $oDbSessionGL->getMaxId($cNameOfIdField, $cTable);

 $oDbSessionGL->TransactionLoop($iNrOfLoopsIfTransactionFails,
                                \&pProcedureToCall,
                                [@lParamsForProcedure]);

 $oDbSessionGL->TransactionLoop($iNrOfLoopsIfTransactionFails,
                                sub
                                {
                                   my $pDbHandle = shift;

                                   # db-actions
                                   ...
                                },
                                [@lParamsForProcedure]);

 $oDbSessionGL->DateToDb(scalar(localtime()));
 DbSession::Pg::DateToDb(undef, scalar(localtime()));

 $oDbSessionGL->DateFromDb($cDateField);
 DbSession::Pg::DateFromDb($cDateField);

 $bAutoCommit = $oDbSession->getAutoCommit();
 $oDbSession->setAutoCommit($bAutoCommit);

 $bPrintError = $oDbSession->getPrintError();
 $oDbSession->setPrintError($bPrintError);

 $bRaiseError = $oDbSession->getRaiseError();
 $oDbSession->setRaiseError($bRaiseError);

 $cDataSource = $oDbSession->getDataSource();

 $pDbHandle = $oDbSession->getDbHandle();

=cut

# The POD text continues at the end of the file.


use WWWdb::Base;

# --------------------------------------------------------------------
# DbSession
# --------------------------------------------------------------------
package DbSession;
use vars qw(%hMethods %hAttribs %hTypeMap @lScanFormats);

# use English;
# use Date::Calc qw(:all);
# use Time::Local;

use strict;

@DbSession::ISA = qw(Base);

{

   @lScanFormats =
       ("%a +%b +%e +%H:%M:%S +%Y.*", # Tue Jan  9 16:37:05 2001
        "%Y:%m:%d:%H:%M:%S%z?", # 2001:01:08:16:37:35
        "%Y-%m-%d +%H:%M:%S%z?", # 2001-01-08 16:37:35
        "%d\\.%m\\.%Y +%H:%M:%S%z?", # 08.01.2001 16:37:35
        "%d\\.%m\\.%y +%H:%M:%S%z?", # 08.01.2001 16:37:35
        "%B, %e %y",            # mmmmmmmm, d yy
        "%B, %e %Y",            # mmmmmmmm, d yyyy
        "%e\\. +%B +%y",        # d. mmmmmmmm yy
        "%e\\. +%B +%Y",        # d. mmmmmmmm yyyy
        "%d",                   # dd
        "%d%m",                 # ddmm
        "%d%m%y",               # ddmmyy
        "%d%m%Y",               # ddmmyyyy
        "%e-%b-%y",             # d-mmm-yy
        "%Y%m%d%H%M%S%z?",      # yyyymmssHHMMSS
        "%e\\.%h\\.%Y?",        # dd.mm.[yyyy]
        "%e\\.%h\\.%y?",        # dd.mm.[yy]
        "%e\\.%h\\.?",          # d[d].m[m][.]
        "%e\\.?",               # d[d][.]
        "%m/%y",
        "%H:%M:%S",
        "%H:%M %p",
        "%H:%M",
        );

   %hAttribs =
       (
        RaiseError       => ["b", "public",   "rw", 0],
        PrintError       => ["b", "public",   "rw", 0],
        AutoCommit       => ["b", "public",   "rw", 1],
        Username         => ["c", "private",  "r",  undef],
        Password         => ["c", "private",  "r",  undef],
        DbHost           => ["c", "private",  "r",  undef],
        DbName           => ["c", "private",  "r",  undef],
        DbDriver         => ["c", "private",  "r",  undef],
        ConversionPlugin => ["o", "private",  "rw", undef],
        StartupPlugin    => ["o", "private",  "rw", undef],
        DbHandle         => ["o", "public",   "r",  undef],
        DataSource       => ["c", "public",   "r",  undef],
        Lang             => ["c", "public",   "r",  "en"],
        DateScanFormats  => ["l", "public",   "rw", \@lScanFormats],
        DatePrintFormat  => ["h", "public",   "rw", "%Y-%m-%d %H:%M:%S"],
        );

}


# --- constructor ---------------------------------------------------
sub DbSession::new()
{
   my $cPkgPI         = shift;
   my $cDbDriverPI    = shift;
   my $cDbNamePI      = shift;
   my $cDbUsernamePI  = shift;
   my $cDbPasswordPI  = shift;

   my $bDriverOk = 0;

   my $cClass   = ref($cPkgPI) || $cPkgPI;

   my $hSelf   = $cClass->SUPER::new(@_);

   $hSelf->{"DbDriver"}     = $cDbDriverPI;
   $hSelf->{"DbName"}       = $cDbNamePI;
   $hSelf->{"Username"}     = $cDbUsernamePI;
   $hSelf->{"Password"}     = $cDbPasswordPI;

   foreach ("Adabas-only-for-testing",
            "Pg",
            "mysql",
            "Oracle",
            "InterBase",
            "ODBC",
            "ODBC_Pg")
   {
      if(/$cDbDriverPI/)
      {
         $bDriverOk = 1;
         require "WWWdb/DbSession/$cDbDriverPI.pm";
      }
   }

   # $hSelf->{"DebugLvl"} = 1;

   die sprintf($hSelf->i18n("Unimplemented Db-Driver %s"), $cDbDriverPI) .
       "\n" .
       $hSelf->i18n("Use one of Pg, mysql, Oracle, ODBC, InterBase")
       unless $bDriverOk;

   push @DbSession::ISA, "DbSession::$cDbDriverPI";

   $hSelf->_Connect();

   bless ($hSelf, $cClass);

   return $hSelf;
}



# --- connect to the database -----------------------------------------------
sub DbSession::_Connect($)
{
   my $hSelfPI   = shift;
   my $cResult   = undef;


   $hSelfPI->_CreateDataSource();

   printf STDERR
       "DbSession::_connect\nUser/Pass: %s/%s\nDataSource: %s\n",
       $hSelfPI->{"Username"},
       $hSelfPI->{"Password"},
       $hSelfPI->{"DataSource"}
   if $hSelfPI->{"DebugLvl"};

   # make a standard connection to the database
   $hSelfPI->{"DbHandle"} = DBI->connect($hSelfPI->{"DataSource"},
                                         $hSelfPI->{"Username"},
                                         $hSelfPI->{"Password"})
       || die($hSelfPI->Error(undef,
                              "connect",
                              ($hSelfPI->{"DataSource"} .
                               ", " .
                               $hSelfPI->{"Username"} .
                               ", " .
                               $hSelfPI->{"Password"}),
                              $hSelfPI->i18n("Connection to Database failed!")));

   # handle long values
   $hSelfPI->{"DbHandle"}->{LongReadLen} = 1024 * 64;
   $hSelfPI->{"DbHandle"}->{LongTruncOk} = 0;

   no strict;
   
   # names and values of all the defined SQL standard types 
   foreach (@{$DBI::EXPORT_TAGS{sql_types}}) 
   {
	   my $iIdType = eval("DBI::$_");

	   $hTypeMap{$iIdType} = $_;

   }

   use strict;
   
   return $cResult;
}


# --- Generate a database-error ---------------------------------------------
sub DbSession::Error($$$$)
{
   my $hSelfPI       = shift;
   my $hHdlPI        = shift;
   my $cSQLStmtPI    = shift;
   my $cBindValuesPI = shift;
   my $cDescript     = shift;

   my $cError;
   my $cErrstr;
   my $cResult    = undef;

   $hHdlPI = $hSelfPI->{"DbHandle"}
      unless defined $hHdlPI;

   if (defined $hHdlPI)
   {

      if($hHdlPI->err())
      {
         $cError  = $hHdlPI->err();
         $cErrstr = $hHdlPI->errstr();
      }
   }
   else
   {

      if(DBI::err())
      {
         $cError  = DBI::err();
         $cErrstr = DBI::errstr();
      }
   }

   $cResult =
       (sprintf($hSelfPI->i18n("Database Error occured during %s!") . "\n" .
                $hSelfPI->i18n("    SQL-Error: (%d) %s") . "\n" .
                $hSelfPI->i18n("    Statement: %s") . "\n" .
                $hSelfPI->i18n("    Bind-val.: %s") . "\n",
                $cDescript,
                $cError,
                $cErrstr,
                $cSQLStmtPI,
                $cBindValuesPI))
           if $cError;

   return $cResult;
}



# --- Execute a Non-select SQL-statement ------------------------------------
sub DbSession::SqlDo($@)
{
   my $hSelfPI      = shift;
   my $cSqlCmdPI    = shift;
   my @lBindParams  = @_;

   my $pStmtHdl;
   my $cResult      = undef;

   $pStmtHdl = $hSelfPI->{"DbHandle"}->prepare($cSqlCmdPI) or
       die $hSelfPI->Error(undef,
                           $cSqlCmdPI,
                           (join ", ", @lBindParams),
                           "SqlDo: prepare");

   printf STDERR
       "DbSession::SqlDo\nStmt: %s\nBind: %s\n",
       $cSqlCmdPI,
       (join ", ", @lBindParams)
           if $hSelfPI->{"DebugLvl"};

   unless ($cResult)
   {
      $cResult = $pStmtHdl->execute(@lBindParams) or
          die $hSelfPI->Error($pStmtHdl,
                              $cSqlCmdPI,
                              (join ", ", @lBindParams),
                              "SqlDo");
   }


   return $cResult;
}



# --- Execute a select SQL-statement ----------------------------------------
sub DbSession::SqlSelect($@)
{
   my $hSelfPI      = shift;
   my $cSqlCmdPI    = shift;
   my @lBindParams  = @_;

   my $pStmtHdl;
   my $cResult      = undef;

   $pStmtHdl = $hSelfPI->{"DbHandle"}->prepare($cSqlCmdPI) or
       die $hSelfPI->Error(undef,
                           $cSqlCmdPI,
                           (join ", ", @lBindParams),
                           "SqlSelect: prepare");

   printf STDERR
       ("DbSession::SqlSelect\nStmt: %s\nBind: '%s'\n",
        $cSqlCmdPI,
        (join "', '", @lBindParams))
           if ($hSelfPI->{"DebugLvl"});

   unless ($cResult)
   {
      $cResult = $pStmtHdl->execute(@lBindParams) or
          die $hSelfPI->Error($pStmtHdl,
                              $cSqlCmdPI,
                              (join ", ", @lBindParams),
                              "SqlSelect");

      $cResult = $pStmtHdl->fetchall_arrayref();
   }

   if ($cResult eq "0E0")
   {
      $cResult = [[undef]];
   }

   if ($hSelfPI->{"DebugLvl"})
   {
      $Data::Dumper::Indent = 1;         # mild pretty print

      print STDERR Data::Dumper::Dumper($cResult);
   }

   return $cResult;
}


# --- Call a procedure in a transaction-Loop -------------------------------------
sub DbSession::TransactionLoop($$$)
{
   my $hSelfPI      = shift;
   my $iNrOfTriesPI = shift;
   my $pProcedurePI = shift;
   my @lParamsPI    = @_;

   my $bAutoCommitSave = $hSelfPI->getAutoCommit();
   my $cResult         = undef;
   my $iIndTry;
   my $pStmtHdl;
   my $cEvalError      = undef;

   $hSelfPI->setAutoCommit(0);
   $hSelfPI->{"DbHandle"}->commit();

   # Try iNrOfTries, if the transaction fails
   for ($iIndTry = 1;
        $iIndTry <= $iNrOfTriesPI;
        $iIndTry ++)
   {

      $cResult = eval {
         &{$pProcedurePI}($hSelfPI, @lParamsPI);
      };

      $cEvalError = $@;

      # there was an error, rollback the transaction
      unless (defined $cResult)
      {
         $hSelfPI->{"DbHandle"}->rollback();
         sleep(5);
      }
      # everything was ok, leave the loop
      else
      {
         $hSelfPI->{"DbHandle"}->commit();
         last;
      }
   }

   $hSelfPI->setAutoCommit($bAutoCommitSave);

   die $cEvalError
       if $cEvalError;

   return $cResult;
}



# --- get the highest id of a table ----------------------------------------
sub DbSession::setAutoCommit($$)
{
   my $hSelfPI       = shift;
   my $bAutoCommitPI = shift;

   my $cResult;

   $hSelfPI->{"DbHandle"}->{AutoCommit} = $bAutoCommitPI;
   $hSelfPI->{AutoCommit} = ($bAutoCommitPI? 1: 0);

   return $cResult;
}



# --- get the highest id of a table ----------------------------------------
sub DbSession::getMaxId($$)
{
   my $hSelfPI      = shift;
   my $cTablePI     = shift;
   my $cFieldPI     = shift;

   my $lResult;
   my $cResult;

   $lResult = $hSelfPI->SqlSelect
       (sprintf("SELECT MAX(%s) FROM %s",
                $cTablePI,
                $cFieldPI));

   $cResult = $lResult->[0]->[0];

   if(defined $cResult)
   {
      $cResult ++;
   }
   else
   {
      $cResult = 1;
   }

   return $cResult;
}



# --- disconnect from database ----------------------------------------------
sub DbSession::_Disconnect($)
{
   my $hSelfPI   = shift;
   my $cResult   = undef;

   if(defined $hSelfPI->{"DbHandle"})
   {
      $hSelfPI->{"DbHandle"}->commit()
          unless ($hSelfPI->getAutoCommit());

      # close the connection to the database
      $hSelfPI->{"DbHandle"}->disconnect()
          || die($hSelfPI->i18n("Disconnect from Database failed!"));
   }

   return $cResult;
}


sub DbSession::ParseDate($$)
{
   my $cDateFormatPI = shift;
   my $cDatePI       = shift;

   my $cDateRegex    = $cDateFormatPI;
   my @lResult       = undef;
   my %hDate;
   my $cToken;
   my %hTokens =
       (
        "%y" => "\\d\\d",          # year (2 digits)
        "%Y" => "\\d\\d\\d\\d",    # year (4 digits)
        "%d" => "\\d\\d",          # numeric day of the month, with leading zeros (eg 01..31)
        "%e" => "\\d{1,2}",        # day of month [1 .. 31]
        "%W" => "\\d{1,2}",        # week number, Monday as first day of week
        "%a" => "[A-Za-z]{3}",     # day of the week abbr
        "%A" => "[A-Za-z]+",       # day of the week
        "%j" => "\\d{1,3}",        # day of the year
        "%w" => "[0-6]",           # day of the week, numerically, Sunday == 0
        "%b" => "[A-Za-z]{3}",     # month abbr
        "%B" => "[A-Za-z]+",       # month
        "%h" => "\\d{1,2}",        # month [1 .. 12]
        "%m" => "\\d\\d",          # month [01 .. 12]
        "%H" => "\\d\\d",          # hour, 24 hour clock, leading 0's)
        "%I" => "\\d\\d",          # hour, 12 hour clock, leading 0's)
        "%k" => "\\d{1,2}",        # hour
        "%l" => "\\d{1,2}",        # hour, 12 hour clock
        "%p" => "[AP]M",           # AM or PM
        "%Z" => "\[A-Za-z]+",      # timezone in ascii. eg: PST
        "%z" => "[-+]\\d{2,4}",    # timezone in format -/+00 .. -/+0000
        "%M" => "\\d\\d",          # minute, leading 0's
        "%S" => "\\d\\d",          # seconds, leading 0's
        "%%" => "%",               # PERCENT
        "%n" => "\\n",             # NEWLINE
        "%t" => "\\t",             # TAB
        );
   my $cError = "";
   my @lReferences = ();

   # now we make a regex from the date-format-string
   foreach $cToken (keys %hTokens)
   {
      $cDateRegex =~ s/$cToken/($hTokens{$cToken})/g;
   }

   $cToken = $cDateFormatPI;

   # now we pull out the differen tokens
   foreach (1 .. 9)
   {
      # is this token valid?
      if($cToken =~ m/(%[%aAbBdehHIjklmMnpStwWyYZz])/)
      {
         # remember it in the list of references
         push @lReferences, $1;

         # replace it by its number
         $cToken =~ s/$1/%$_/g;
      }
   }

   # now we look it the date matches
   if ($cDatePI =~ /^$cDateRegex$/)
   {
      my $iInd = 1;
      my %hDateMap =
          ("%a" => "wday", # day of the week abbr
           "%A" => "wday", # day of the week
           "%b" => "mon",  # month abbr
           "%B" => "mon",  # month
           "%d" => "mday", # numeric day of the month, with leading zeros (eg 01..31)
           "%e" => "mday", # numeric day of the month, without leading zeros (eg 1..31)
           "%h" => "mon",  # month abbr
           "%H" => "hour", # hour, 24 hour clock, leading 0's)
           "%I" => "hour", # hour, 12 hour clock, leading 0's)
           "%j" => "yday", # day of the year
           "%k" => "hour", # hour
           "%l" => "hour", # hour, 12 hour clock
           "%m" => "mon",  # month number, starting with 1
           "%M" => "min",  # minute, leading 0's
           "%p" => "am_pm",# AM or PM
           "%S" => "sec",  # seconds, leading 0's
           "%w" => "wday", # day of the week, numerically, Sunday == 0
           "%W" => "week", # week number, Monday as first day of week
           "%y" => "year", # year (2 digits)
           "%Y" => "year", # year (4 digits)
           "%Z" => "tz",   # timezone in ascii. eg: PST
           "%z" => "tz"    # timezone in format -/+0000
           );

      # now we have to scan all the references we have found before
      foreach $cToken (@lReferences)
      {
         no strict;
         my $cReplace = "${$iInd}";
         use strict;

         # check all date-tokens, if we have a value to check
         if($cReplace)
         {
            foreach (keys %hDateMap)
            {

               if ($cToken eq $_)
               {
                  $hDate{$hDateMap{$_}} = $cReplace;
               }
            }
         }

         $iInd ++;
      }

      if (!defined  $hDate{"hour"} &&
          !defined  $hDate{"min"} &&
          !defined  $hDate{"sec"})
      {
         $hDate{"hour"} = "00";
         $hDate{"min"}  = "00";
         $hDate{"sec"}  = "00";
      }

      if (!defined  $hDate{"mday"} &&
          !defined  $hDate{"mon"} &&
          !defined  $hDate{"year"})
      {
         $hDate{"year"}  = (localtime)[5] + 1900;
         $hDate{"mon"}   = (localtime)[4] + 1;
         $hDate{"mday"}  = (localtime)[3];
      }

      if (defined  $hDate{"mday"} &&
          !defined  $hDate{"mon"} &&
          !defined  $hDate{"year"})
      {
         $hDate{"year"}  = (localtime)[5] + 1900;
         $hDate{"mon"}  = (localtime)[4] + 1;
      }

      if (defined  $hDate{"mday"} &&
          defined  $hDate{"mon"} &&
          !defined  $hDate{"year"})
      {
         $hDate{"year"}  = (localtime)[5] + 1900;
      }

      if (defined  $hDate{"year"})
      {
         if($hDate{"year"} < 100)
         {
            $hDate{"year"} += (($hDate{"year"} < 70)? 2000: 1900);
         }
      }
      else
      {
         $hDate{"year"} = "0";
      }

      if (defined  $hDate{"mon"})
      {
         if ($hDate{"mon"} =~ /\d+/)
         {
            if ($hDate{"mon"} < 1)
            {
               # trick for translation: i18n("Month to small");
               $cError = "Month to small";
            }
            elsif ($hDate{"mon"} > 12)
            {
               # trick for translation: i18n("Month to big");
               $cError = "Month to big";
            }
         }
         else
         {
            my $cLang;
            my $cMon;

            my %hMon =
                ("en" => {
                   "jan(uary)?"   => 1,
                   "feb(ruary)?"  => 2,
                   "mar(ch)?"     => 3,
                   "apr(il)?"     => 4,
                   "may"          => 5,
                   "june?"        => 6,
                   "july?"        => 7,
                   "aug(ust)?"    => 8,
                   "sep(tember)?" => 9,
                   "oct(ober)?"   => 10,
                   "nov(ember)?"  => 11,
                   "dec(ember)?"  => 12, },
                 "de" => {
                    "jan(uar)?"      => 1,
                    "feb(ruar)?"     => 2,
                    "mrz|märz|maerz" => 3,
                    "apr(il)?"       => 4,
                    "mai"            => 5,
                    "juni?"          => 6,
                    "juli?"          => 7,
                    "aug(ust)?"      => 8,
                    "sep(tember)?"   => 9,
                    "okt(ober)?"     => 10,
                    "nov(ember)?"    => 11,
                    "dez(ember)?"    => 12, },
                 );


            LANG: foreach $cLang ("en", "de")
            {
               foreach $cMon (keys %{$hMon{$cLang}})
               {
                  if ($hDate{"mon"} =~ /^$cMon$/i)
                  {
                     $hDate{"mon"} = $hMon{$cLang}{$cMon};
                     last LANG;
                  }
               }
            }

            unless ($hDate{"mon"} =~ /\d+/)
            {
               # trick for translation: i18n("Month unknown");
               $cError = "Month unknown";
               $hDate{"mon"} = 0;
            }
         }
      }
      else
      {
         $hDate{"mon"} = "0";
      }

      if (defined  $hDate{"mday"})
      {
         my @lDaysPerMon =
             (0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

         if ($hDate{"mday"} < 1)
         {
            # trick for translation: i18n("Day to small");
            $cError = "Day to small";
         }
         elsif ($hDate{"mon"} &&
                $hDate{"mday"} > $lDaysPerMon[$hDate{"mon"}])
         {
            # trick for translation: i18n("Day to big");
            $cError = "Day to big";

            # FIXME: Insert lap-year-rules here ...
         }
      }
      else
      {
         $hDate{"mday"} = "0";
      }


      if (defined  $hDate{"hour"})
      {
         if (defined  $hDate{"am_pm"})
         {
            if (lc($hDate{"am_pm"}) eq "pm")
            {
               $hDate{"hour"} += 11;
            }
            else
            {
               $hDate{"hour"} --;
            }
         }

         if ($hDate{"hour"} < 0)
         {
            # trick for translation: i18n("Hour to small");
            $cError = "Hour to small";
         }
         elsif ($hDate{"hour"} > 23)
         {
            # trick for translation: i18n("Hour to big");
            $cError = "Hour to big";
         }
      }

      if (defined  $hDate{"min"})
      {
         if ($hDate{"min"} < 0)
         {
            # trick for translation: i18n("Minute to small");
            $cError = "Minute to small";
         }
         elsif ($hDate{"min"} > 59)
         {
            # trick for translation: i18n("Minute to big");
            $cError = "Minute to big";
         }
      }

      if (defined  $hDate{"sec"})
      {
         if ($hDate{"sec"} < 0)
         {
            # trick for translation: i18n("Second to small");
            $cError = "Second to small";
         }
         elsif ($hDate{"sec"} > 59)
         {
            # trick for translation: i18n("Second to big");
            $cError = "Second to big";
         }
      }
   }
   else
   {
      $cError = sprintf("Date does not match %s", $cDateRegex);
   }

   if ($cError)
   {
      @lResult = "$cError";
   }
   else
   {
      @lResult = ($hDate{"sec"},
                  $hDate{"min"},
                  $hDate{"hour"},
                  $hDate{"mday"},
                  $hDate{"mon"},
                  $hDate{"year"},
                  0,
                  0,
                  -1);           # -1 means: no info about DST
   }

   return @lResult;
}


# --- Give out the date like strftime does --------------------------------------
sub DbSession::PrintDate($@)
{
   my $cFormatPI    = shift;
   my @lLocaltimePI = @_;

   my $cResult = undef;

   # when the list has only one element, this is an error-message
   if (defined $lLocaltimePI[1])
   {

      # correct the month
      if ($lLocaltimePI[4])
      {
         $lLocaltimePI[4] --;
      }

      # correct the year
      if ($lLocaltimePI[5] > 1900)
      {
         $lLocaltimePI[5] -= 1900;
      }

      $cResult = POSIX::strftime($cFormatPI, @lLocaltimePI);
      # print STDERR "strftime: $cFormatPI -> (@lLocaltimePI) -> $cResult\n";
   }
   else
   {
      # die "Can't format with '$cFormatPI'";
      $cResult = "",
   }

   return $cResult;

}

# --- Scan a set of date-formats -------------------------------------------
sub DbSession::ScanDate($;@)
{

   my $cDatePI         = shift;
   my @lScanFormatsPI  = @_;

   my $cFormat;
   my @lResult       = "";
   my @lScanFormats = @lScanFormatsPI;

   @lScanFormats = @DbSession::lScanFormats
	   unless @lScanFormats;

   foreach $cFormat (@lScanFormats)
   {

      @lResult = &DbSession::ParseDate($cFormat, $cDatePI);

      last
          if defined $lResult[1];

   }

   return @lResult;
}



# --- standard DESTROY-method -----------------------------------------------
sub DbSession::DESTROY($)
{
   my $hSelfPI   = shift;

   $hSelfPI->_Disconnect();

}


# --- standard DESTROY-method -----------------------------------------------
sub DbSession::NameOfType($$)
{
	my $iDataTypePI = shift;
	
	return $hTypeMap{$iDataTypePI};
}

1;


__END__


=head1 DESCRIPTION

=head1 The DbSession class

Db-Session is another layer over the DBI-Module. It gives some
easy-to-use functionality for db-access.

=head2 Note

=over 4


When a DbSession-object gets destroyed, the session will be
automatically finished.

=back

=head2 Class Methods

=over 4

=item B<new>

 $oDbSession     = DbSession->new($cDatabaseType,
                                  $cDatabaseName,
                                  $cDatabaseUser,
                                  $cDatabasePassword,
                                  RaiseError       => $bRaiseError,
                                  PrintError       => $bPrintError,
                                  AutoCommit       => $bAutoCommit);

Create a new DbSession and connect to the database.
The following database-types are supported currently:

 - PostgreSQL
 - MySql
 - Oracle

The attributes are passed to the internal DBI-Object.

=item B<Error>

 $oDbSession->Error($pHandle,
                    $cSQL_Statement,
                    $cBindValues,
                    $cDescription);

This function generates an error-message, if a problem occured in the
last database statement. You will get the text and the number of the
database-error and the statement together with the data passed by
bind, which led to this error.

Normally this method is only called internally.

=item B<SqlDo>

 $oDbSession->SqlDo($cSQL_Statement,
                    [@lBindValues]);

SqlDo executes a non-select SQL-statement passed in $cSQL_Statement.
If you want to pass Parameters, then you can generate the statement by
example via sprintf, but you have to quote them by yourselve then.

The second possibility (which is recommended and much easier)
is, to pass the parameters separately. The underlying DBI
will care about quoting etc.

Here are two examples which show the difference:

 $oDbSession->SqlDo
     (sprintf("INSERT INTO customer
               (id_customer,
                name,
                last_change)
               VALUES
               (%d, '%s', '%s')",
              $iIdCustomer,
              $cCustomerName,
              $cLastChange));

Here you have to build your SQL-Statement by yourself and you
need to quote the fields correctly.

 $oDbSession->SqlDo
     ("INSERT INTO customer
      (id_customer,
       name,
       last_change)
      VALUES
      (?, ?, ?)",
      $iIdCustomer,
      $cCustomerName,
      $cLastChange);

Here, you only have to pass the SQL-statement and the parameters
and ... thats it!

If some database-error occurs, the procedure will throw a die-signal
with an error-message via the Error-method.

=item B<SqlSelect>

 $lResult = $oDbSession->SqlSelect($cSQL_Statement,
                                   [@lBindValues]);


This statement is a special-form of the SqlDo-statement. It prepares a
SELECT-statement passed in $cSQL_Statement, executes it and returns
the result in a array of arrays.

To process the results the following loops may be helpful:

 # loop through all records
 foreach $pRecord (@$lResult)
 {
    # loop through the fields
    foreach $cField (@$pRecord)
    {
       print "$cField, ";

    }
    print "\n";
 }

To access a value direcly you may use:

 print "Second Record <@{$lResult->[1]}>\n";
 print "Third Record, Second Field <$lResult->[2]->[1]>\n";

If some database-error occurs, the procedure will throw a die-signal
with an error-message via the Error-method.

=item B<TransactionLoop>

 $oDbSessionGL->TransactionLoop($iNrOfLoopsIfTransactionFails,
                                \&pProcedureToCall,
                                [@lParamsForProcedure]);

or with a anonymous subroutine

 $oDbSessionGL->TransactionLoop($iNrOfLoopsIfTransactionFails,
                                sub
                                {
                                   my $pDbHandle = shift;

                                   # db-actions
                                   ...
                                },
                                [@lParamsForProcedure]);

This procedure disables (temporarily) the AutoComit
functionality. Then it runs the procedure given by
\&pProcedureToCall. The first parameter for this procedure is a
pointer to the DbSession-object it belongs to. Then any other
parameters, defined by the user may follow.  If that procedure thows a
die or returns undef, the transaction is assumed to be failed. In this
case it will be returned up to $iNrOfLoopsIfTransactionFails times.

If the db-error keeps happening, then this procedure throws a
die with the SQL-errors.

=item B<getMaxId>

 $iMaxId = $oDbSessionGL->getMaxId($cNameOfIdField, $cTable);

Especially when you use databases, which do not support auto-increment
fields, or if you want to keep your portability, then this will help
you to generate id\'s for unique indexes in a simple way.

Passing the name of the unique key-field in the variable
$cNameOfIdField and the database-table in $cTable, this procedure
selects the maximum value for this field, adds one and returns the
value.

Please note that this is only a hack, because if two users use this
function at the same time, duplicate values may be generated. A better
way would be to have a own table for this index-fields and use them
like tickets in secure transactions.

=item B<_Connect>

This internal procedure creates a datasource-descriptor for the
database and connects to the database.

This procedure is for internal use only.

=item B<_Disconnect>

This internal procedure commits all open transactions and disconnects
from the database.

This procedure is for internal use only.

=item B<[DATABASE]::_CreateDataSource>


This internal procedure generates the necessary DBI-datasource for
the specified database.

This procedure is for internal use only.

=item B<[DATABASE]::DateToDb>

 $oDbSessionGL->DateToDb(scalar(localtime()));

or the specialized form for a database

 DbSession::Pg::DateToDb(undef, scalar(localtime()));

Converts a date, given by the perl-localtime-function (b.e. Mon Mar 6
16:21:21 2000) to a date-format, the database can understand.

The following date-data-types are supported

         +------------+-----------+
         | Database   | Date-type |
         +------------+-----------+
         | PostgreSQL | datetime  |
         | Oracle     | timestamp |
         | MySql      | datetime  |
         +------------+-----------+


=item B<[DATABASE]::DateFromDb>

 $oDbSessionGL->DateFromDb($cDateField);

or the specialized form for a database

 DbSession::Pg::DateFromDb($cDateField);

This function is the opposit to DateToDb. It converts a date-field
as delivered by the database to the same format,
the perl-localtime-function would give back.

=back

=head2 Class Attributes

=over 4

=item B<AutoCommit>

 $bAutoCommit = $oDbSession->getAutoCommit();
 $oDbSession->setAutoCommit($bAutoCommit);

If a commit-statement should be issued automatically after
each SQL-statement then AutoCommit must be set to a true
value (which it is by default)

=item B<PrintError>

 $bPrintError = $oDbSession->getPrintError();
 $oDbSession->setPrintError($bPrintError);

Reads or sets the DBI::PrintError flag.

=item B<RaiseError>

 $bRaiseError = $oDbSession->getRaiseError();
 $oDbSession->setRaiseError($bRaiseError);

Reads or sets the DBI::RaiseError flag.

=item B<DataSource>

 $cDataSource = $oDbSession->getDataSource();

Returns the name of the DBI::Datasource.

=item B<DbHandle>

 $pDbHandle = $oDbSession->getDbHandle();

Returns the database-handle for the current connection.

=back


