#!/usr/bin/perl -w
# -----------------------------------------------------------------------------
# TWC
# Treuwert-Computer GmbH
# Schloßbergring 9
# 79098 Freiburg
#
# Author: Klaus Reger <K.Reger@twc.de>
#
# -----------------------------------------------------------------------------
# Program-Data
# -----------------------------------------------------------------------------
# Project  :
# System   :
# Program  :
# Module   : $RCSfile: ConfigFile.pm,v $
# Version  : $Revision: 1.4 $
# Date     : $Date: 2002/02/25 10:50:44 $
# State    : $State: Exp $
#
# Description:
#   This module handles all the things of WWWdb, which have
#   to do with ConfigFiles
#
# Change-log (See at end of this file)
#
# --- Known Bugs -------------
# please look at the BUGS-File
# --- TODO -------------------
# please look at the TODO-File
# ----------------------------------------------------------------------------

=head1 NAME

WWWdb::ConfigFile - ConfigFiles for WWWdb

=head1 SYNOPSIS

 use WWWdb::ConfigFile;

 $oConfigFile = ConfigFile->new($cFileName,
                                [ReadOnly  => $bReadOnly,]
                                [MustExist => $bMustExist]);

 $cValue      = $oConfigFile->GetValue($cSection,
                                       $cEntry);

 @lValue      = $oConfigFile->GetList($cSection,
                                      $cEntry);

 $cValue      = $oConfigFile->SetValue($cSection,
                                       $cEntry,
                                       $cValue);

 $oConfigFile->DESTROY>();

=cut

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



use WWWdb::Base;
use WWWdb::Hash;

use strict;




# --------------------------------------------------------------------
# ConfigFile::EntryAttr
# --------------------------------------------------------------------
package ConfigFile::EntryAttr;
use vars qw(%hMethods %hAttribs);

@ConfigFile::EntryAttr::ISA = qw(Base);

{
   %hAttribs =
       (
        CommentBefore => ["c", "public",  "rw", undef],
        CommentAfter  => ["c", "public",  "rw", undef],
        FromFile      => ["b", "public",  "r",  0],
        SortNo        => ["i", "public",  "rw", 0]
        );
}


# --- constructor ---------------------------------------------------
sub ConfigFile::EntryAttr::new()
{
   my ($cPkgPI)      = shift;

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

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

   return $hSelf;
}




# --------------------------------------------------------------------
# ConfigFile
# --------------------------------------------------------------------
package ConfigFile;
use vars qw(%hMethods %hAttribs);

@ConfigFile::ISA = qw(ConfigFile::EntryAttr); # implies Base

{
    %hAttribs =
        (
         ReadOnly      => ["b", "public",  "r", 0],
         RewriteFile   => ["b", "public",  "r", 0],
         MustExist     => ["b", "public",  "r", 0],
         FileName      => ["c", "public",  "r", undef],
         Sections      => ["o", "public",  "r", undef],
         FileFormat    => ["c", "public",  "r", "ini"],
         MaxSortNoSect => ["d", "public",  "rw", 0],
      );
}


# --- constructor ---------------------------------------------------
sub ConfigFile::new()
{
   my ($cPkgPI)      = shift;
   my $cFileNamePI   = shift;

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

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

   bless ($hSelf, $cClass);

   die ($hSelf->i18n("How should I read a config-file, " .
                     "if I don't know where it is?\n"))
       unless defined $cFileNamePI;

   $hSelf->{"FileName"} = $cFileNamePI;
   $hSelf->{"Sections"} = Hash->new();

   $hSelf->ReadFile_Ini();

   return $hSelf;
}


# --- Read in a config-file in windowze-ini style ------------------------
sub ConfigFile::ReadFile_Ini()
{

   my $hSelfPI    = shift;

   my $cResult = undef;

   my $bEntryNew;
   my $bSectionNew;
   my $cCommentAfter;
   my $cCommentBefore;
   my $cEntry;
   my $cEofMarker;
   my $cSection;
   my $cSubSection;
   my $cValue;
   my $iLastMaxSpaceLine = 0;
   my $iLineNr;
   my $iMaxSpaceCnt = 0;
   my $iSpaceCnt = 0;
   my $oCurrEntry;
   my $oCurrSection;
   my @lComment;
   my $iSectionSortNo = 0;
   my $iEntrySortNo = 0;


   if(! -f $hSelfPI->{"FileName"} && $hSelfPI->{"MustExist"})
   {
        die (sprintf($hSelfPI->i18n("Sorry, but config-file %s ".
                                    "does not exist!"),
                     $hSelfPI->{"FileName"}));

   }

   if(! -r $hSelfPI->{"FileName"} && $hSelfPI->{"MustExist"})
   {
        die (sprintf($hSelfPI->i18n("Sorry, but I cant read " .
                                    "config-file %s!"),
                     $hSelfPI->{"FileName"}));

   }

   unless (open CONFIG_FILE, "<" . $hSelfPI->{"FileName"})
   {
      return $cResult;
   }

   $iLineNr = 0;
   while (<CONFIG_FILE>)
   {
      chomp;
      $iLineNr ++;

      printf STDERR ">>> $_\n"
          if $hSelfPI->{"DebugLvl"};

      # Append the following Line to the Data
      if($cEofMarker)
      {
         if ($_ eq $cEofMarker)
         {
            $cEofMarker = undef;
            $bEntryNew  = 1;

            # remove the last \n

            $cValue =~ s/\n$//;

         }
         else
         {
            $cValue    .= $_ . "\n";
         }
      }
      # Comments
      elsif (/^[;\#]\s*(.*)/)
      {
         push @lComment, $1;
         $iSpaceCnt = 0;
      }
      # empty lines are threated as comments
      elsif (/^$/)
      {
         push @lComment, "";
         $iSpaceCnt ++;

         if ($iSpaceCnt > $iMaxSpaceCnt)
         {
            $iMaxSpaceCnt = $iSpaceCnt;
            $iLastMaxSpaceLine = @lComment;
         }
      }
      # Section
      elsif (/^\[(.*)\]$/)
      {
         $cSection = $1;

         $bSectionNew = 1;
      }
      # Assignment (multi line)
      elsif (/^(\w+)\s*=\s*<<\s*(.*)\s*$/)
      {

         $cEntry = $1;
         $cValue = "";
         $cEofMarker = $2;
      }
      # Assignment (single line)
      elsif (/^(\w+)\s*=\s*(.*)\s*$/)
      {
         $bEntryNew = 1;

         $cEntry = $1;
         $cValue = $2;
      }
      else
      {
         # ERROR
         die ("\n" .
              sprintf($hSelfPI->i18n("Unknown configuration-file ".
                                     "entry at line %d of file %s"),
                      $iLineNr,
                      $hSelfPI->{"FileName"})
              . "\n");
      }

      if ($bSectionNew || $bEntryNew)
          # HandleComments
      {
         # Split the comments
         $cCommentAfter =
             join ("\n",
                   @lComment[0 .. ($iLastMaxSpaceLine - 1)]);
         $cCommentBefore =
             join ("\n", @lComment[($iLastMaxSpaceLine) ..
                                   @lComment]);
         if(defined $oCurrEntry)
         {
            $oCurrEntry->setCommentAfter($cCommentAfter);
            # $oCurrEntry = undef;
         }
         elsif(defined $oCurrSection)
         {
            $oCurrSection->setCommentAfter($cCommentAfter);
         }
         else
         {
            $hSelfPI->setCommentBefore($cCommentAfter);
         }
      }

      # Split the comments
      if ($bSectionNew || $bEntryNew)
      {
         $cCommentAfter =
             join ("\n",
                   @lComment[0 .. ($iLastMaxSpaceLine - 1)]);
         $cCommentBefore =
             join ("\n", @lComment[($iLastMaxSpaceLine) ..
                                   @lComment]);
         if ($cCommentAfter)
         {
            if(defined $oCurrEntry)
            {
               $oCurrEntry->setCommentAfter($cCommentAfter);
               $oCurrEntry = undef;
            }
            elsif(defined $oCurrSection)
            {
               $oCurrSection->setCommentAfter($cCommentAfter);
            }
            else
            {
               $hSelfPI->setCommentBefore($cCommentAfter);
            }
         }
      }

      # last line was a section-declaration
      if ($bSectionNew)
      {
         # generate a new sort-number
         my $iSectionSortNo =   $hSelfPI->getMaxSortNoSect() + 10;

         $oCurrSection =
           ConfigFile::Section->new
               ($cSection, $iSectionSortNo,
                FromFile      => 1,
                CommentBefore => $cCommentBefore);

         $hSelfPI->{"Sections"}->Set($cSection, $oCurrSection);
         $hSelfPI->setMaxSortNoSect($iSectionSortNo);

         $bSectionNew = 0;
         $iSpaceCnt   = 0;

         @lComment = ();
         $iMaxSpaceCnt = 0;
         $iSpaceCnt = 0;
      }


      if ($bEntryNew)
      {
         my $iEntrySortNo =   $oCurrSection->getMaxSortNoEnt() + 10;

         $oCurrEntry =
           ConfigFile::Entry->new
               ($cEntry, $cValue, $iEntrySortNo,
                FromFile      => 1,
                CommentBefore => $cCommentBefore);

         $oCurrSection->getEntries()->Set($cEntry, $oCurrEntry);
         $oCurrSection->setMaxSortNoEnt($iEntrySortNo);

         $bEntryNew   = 0;
         $iSpaceCnt   = 0;
         $cEntry      = "";
         $cValue      = "";

         @lComment = ();
         $iMaxSpaceCnt = 0;
         $iSpaceCnt = 0;
      }
   }

   # HandleComments
   {
      # Split the comments
      $cCommentAfter =
          join ("\n",
                @lComment[0 .. ($iLastMaxSpaceLine - 1)]);
      $cCommentBefore =
          join ("\n", @lComment[($iLastMaxSpaceLine) ..
                                @lComment]);
      if(defined $oCurrEntry)
      {
         $oCurrEntry->setCommentAfter($cCommentAfter);
         # $oCurrEntry = undef;
      }
      elsif(defined $oCurrSection)
      {
         $oCurrSection->setCommentAfter($cCommentAfter);
      }
      else
      {
         $hSelfPI->setCommentBefore($cCommentAfter);
      }
   };

   $hSelfPI->setCommentAfter($cCommentBefore)
       if $cCommentBefore;

   return $cResult;



}


# --- Read in a config-file in windowze-ini style ------------------------
sub ConfigFile::WriteFile_Ini(;$)
{
   my $hSelfPI    = shift;

   my $cSection;
   my $cEntry;
   my $iIndSect = 0;

   open CONFIG_FILE, ">" . $hSelfPI->{"FileName"}. ".new"
       or die ("Can't open new config-file: " .
               $hSelfPI->{"FileName"}. ".new");

   printf CONFIG_FILE &WriteComments_Ini($hSelfPI->getCommentBefore());
   printf CONFIG_FILE "\n" x 3
       if $hSelfPI->getCommentBefore();

   foreach $cSection ($hSelfPI->getSectionNames())
   {
      my $oSection;

      $oSection = $hSelfPI->getSections()->Get("$cSection");

      printf CONFIG_FILE &WriteComments_Ini($oSection->getCommentBefore());
      printf CONFIG_FILE "[%s]\n", $oSection->getName();
      printf CONFIG_FILE &WriteComments_Ini($oSection->getCommentAfter());

      foreach $cEntry ($oSection->getEntryNames())
      {
         my $oValue;

         $oValue = $oSection->getEntries()->Get("$cEntry");

         next
             unless defined $oValue->getValue();

         printf CONFIG_FILE &WriteComments_Ini($oValue->getCommentBefore());

         if($oValue->isMultiLine())
         {
            printf CONFIG_FILE ("%-15s = <<EOF\n%s\nEOF\n",
                    $cEntry,
                    $oValue->getValue());
         }
         else
         {
            my $cTextDelim = (($oValue->getValue() =~ /.* .*/ ||
                               $oValue->getValue() eq "")?
                              '"': "");

            printf CONFIG_FILE ("%-15s = %s%s%s\n",
                                $cEntry,
                                $cTextDelim,
                                $oValue->getValue(),
                                $cTextDelim);
         }
         printf CONFIG_FILE &WriteComments_Ini($oValue->getCommentAfter());
      }

      printf CONFIG_FILE "\n" x 1;

      $iIndSect ++;
   }
   printf CONFIG_FILE &WriteComments_Ini($hSelfPI->getCommentAfter());

   sub WriteComments_Ini()
   {
      my $cCommentPI = shift;
      my $cResult = "";

      foreach (split /\n/, $cCommentPI)
      {
         $cResult .= sprintf ("%s%s\n",
                              $_? "# ": "",
                              $_);
      }

      return $cResult;
   }

   close CONFIG_FILE;

}




# --- Read in a config-file in windowze-ini style ------------------------
sub ConfigFile::WriteFile_XML(;$)
{
   my $hSelfPI    = shift;

   my $cSection;
   my $cEntry;
   my $iIndSect = 0;

   printf "<CONFIG_FILE>\n";

   printf &WriteEntry_XML("COMMENT_BEFORE", 1,
                          $hSelfPI->getCommentBefore());

   foreach $cSection ($hSelfPI->getSectionNames())
   {
      my $oSection;

      $oSection = $hSelfPI->getSections()->Get("$cSection");

      printf "  <SECTION>\n";

      printf &WriteEntry_XML("COMMENT_BEFORE", 2,
                             $oSection->getCommentBefore());
      printf &WriteEntry_XML("NAME", 2,
                             $oSection->getName());
      printf &WriteEntry_XML("COMMENT_AFTER", 2,
                             $oSection->getCommentAfter());

      foreach $cEntry ($oSection->getEntryNames())
      {
         my $oValue;

         $oValue = $oSection->getEntries()->Get("$cEntry");

         printf "    <ENTRY>\n";

         printf &WriteEntry_XML("COMMENT_BEFORE", 3,
                                $oValue->getCommentBefore());
         printf &WriteEntry_XML("NAME", 3,
                                $oValue->getName());
         printf &WriteEntry_XML("VALUE", 3,
                                $oValue->getValue());
         printf &WriteEntry_XML("COMMENT_AFTER", 3,
                                $oValue->getCommentAfter());

         printf "    </ENTRY>\n";
      }

      printf "  </SECTION>\n";

      $iIndSect ++;
   }

   printf &WriteEntry_XML("COMMENT_AFTER", 1,
                          $hSelfPI->getCommentAfter());

   printf "</CONFIG_FILE>\n";



   sub WriteEntry_XML()
   {
      my $cNamePI      = shift;
      my $iIndentionPI = shift;
      my $cValuePI     = shift;

      my $cResult = "";

      $cResult .= sprintf ("%s<%s>\n",
                           " " x ($iIndentionPI * 2),
                           $cNamePI);

      $cResult .= sprintf ("%s%s\n",
                           " " x ($iIndentionPI * 2 + 2),
                           $cValuePI);

      $cResult .= sprintf ("%s</%s>\n",
                           " " x ($iIndentionPI * 2),
                           $cNamePI);

      return $cResult;

   }

}




# --- get all Sections of the Config-File -------------------------------------
sub ConfigFile::getSectionNames($$)
{
   my $hSelfPI    = shift;
   my $cSectionPI = shift;
   my $cNamePI    = shift;


   # Sort the Sections by its sort-names
   my @lResult = sort {
      $hSelfPI->getSections()->Get($a)->getSortNo() <=>
          $hSelfPI->getSections()->Get($b)->getSortNo()
           } $hSelfPI->{"Sections"}->Get();

   return @lResult;
}




# --- Get Value of a ConfigFile-element ---------------------------------------
sub ConfigFile::getValue($$;$)
{
   my $hSelfPI    = shift;
   my $cSectionPI = shift;
   my $cEntryPI   = shift;
   my $cDefaultPI = shift;

   my $oSection;
   my $oEntry;

   my $cResult = undef;

   $oSection = $hSelfPI->getSections()->Get("$cSectionPI");

   if ($oSection)
   {
      $oEntry   = $oSection->getEntries()->Get("$cEntryPI");

      if ($oEntry)
      {
         $cResult = $oEntry->getValue();
      }
   }

   # Set a default-value
   if (!defined $cResult)
   {
      $cResult = $cDefaultPI;
   }

   return $cResult;
}



# --- Get Value of a ConfigFile-element ---------------------------------------
sub ConfigFile::setValue($$$)
{
   my $hSelfPI    = shift;
   my $cSectionPI = shift;
   my $cEntryPI   = shift;
   my $cValuePI   = shift;

   my $oSection;
   my $oEntry;

   my $cResult = undef;

   die $hSelfPI->i18n("setValue not allowed in a read-only config-file!")
       if $hSelfPI->{"ReadOnly"};

   $oSection = $hSelfPI->getSections()->Get("$cSectionPI");

   # Create section if it does not exist
   if (!$oSection)
   {
      my $iSectionSortNo =   $hSelfPI->getMaxSortNoSect();

      $oSection =
        ConfigFile::Section->new
            ($cSectionPI, $iSectionSortNo + 10,
             FromFile      => 0);

      $hSelfPI->{"Sections"}->Set($cSectionPI, $oSection);
      $hSelfPI->setMaxSortNoSect($iSectionSortNo + 10);


   }


   $oEntry   = $oSection->getEntries()->Get("$cEntryPI");

   # Create entry if it does not exist
   if (!$oEntry)
   {
      my $iEntrySortNo =   $oSection->getMaxSortNoEnt();

      $oEntry =
        ConfigFile::Entry->new
            ($cEntryPI, $cValuePI, $iEntrySortNo + 10,
             FromFile      => 0);

      $oSection->getEntries()->Set($cEntryPI, $oEntry);
      $oSection->setMaxSortNoEnt($iEntrySortNo + 10);


   }
   else
   {
      $cResult = $oEntry->setValue("$cValuePI");
   }

   return $cResult;
}




# --- Get Value of a ConfigFile-element ---------------------------------------
sub ConfigFile::deleteSection($)
{
   my $hSelfPI    = shift;
   my $cSectionPI = shift;

   my $oSection;

   my $cResult = undef;

   die $hSelfPI->i18n("deleteSection not allowed in a read-only config-file!")
       if $hSelfPI->{"ReadOnly"};

   $oSection = $hSelfPI->getSections()->Get("$cSectionPI");

   # Create section if it does not exist
   if ($oSection)
   {
      $oSection->delete();

      $hSelfPI->getSections()->Delete("$cSectionPI");
   }

   return $cResult;
}



# --- Get Value of a ConfigFile-element ---------------------------------------
sub ConfigFile::deleteValue($$)
{
   my $hSelfPI    = shift;
   my $cSectionPI = shift;
   my $cEntryPI   = shift;

   my $oSection;
   my $oEntry;

   my $cResult = undef;

   die $hSelfPI->i18n("deleteValue not allowed in a read-only config-file!")
       if $hSelfPI->{"ReadOnly"};

   $oSection = $hSelfPI->getSections()->Get("$cSectionPI");

   # Create section if it does not exist
   if ($oSection)
   {


      $oEntry   = $oSection->getEntries()->Get("$cEntryPI");

      # Create entry if it does not exist
      if ($oEntry)
      {
         $oEntry->delete();
         $oSection->getEntries()->Delete("$cEntryPI");
      }
   }

   return $cResult;
}




sub ConfigFile::CreateSectionAndEntries(@)
{
   my $hSelfPI       = shift;
   my @lValuesPI     = @_;

   my $lAtt;
   my $lValues;

   foreach $lAtt (@lValuesPI)
   {

      foreach $lValues (@{$lAtt->[1]})
      {

         $hSelfPI->setValue($lAtt->[0], $lValues->[0], $lValues->[1]);

      }
   }
}






# --------------------------------------------------------------------
# ConfigFile::Section
# --------------------------------------------------------------------
package ConfigFile::Section;
use vars qw(%hMethods %hAttribs);

@ConfigFile::Section::ISA = qw(ConfigFile::EntryAttr); # implies Base

{

    %hAttribs =
      (
       Name         => ["c", "public",  "r",  undef],
       Entries      => ["o", "public",  "r",  undef],
       MaxSortNoEnt => ["d", "public",  "rw", 0],
      );
}



# --- constructor ---------------------------------------------------
sub ConfigFile::Section::new()
{
   my ($cPkgPI)   = shift;
   my $cNamePI    = shift;
   my $iSortNoPI  = shift;

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

   my $hSelf   = $cClass->SUPER::new
       (@_, "SortNo" => $iSortNoPI);

   bless ($hSelf, $cClass);

   $hSelf->{"Name"}    = $cNamePI;
   $hSelf->{"Entries"}  = Hash->new();

   return $hSelf;
}


# --- destructor ---------------------------------------------------------
sub ConfigFile::Section::delete()
{
   my $hSelfPI      = shift;

   undef $hSelfPI;

   return undef;
}


# --- Append a ConfigFile-element ----------------------------------------------
sub ConfigFile::Section::getEntryNames()
{
   my $hSelfPI    = shift;

   # sort the names by its sort-number
   return sort {
      $hSelfPI->getEntries()->Get($a)->getSortNo() <=>
          $hSelfPI->getEntries()->Get($b)->getSortNo()
           } $hSelfPI->{"Entries"}->Get();
}



# --------------------------------------------------------------------
# ConfigFile::Entry
# --------------------------------------------------------------------
package ConfigFile::Entry;
use vars qw(%hMethods %hAttribs);

@ConfigFile::Entry::ISA = qw(ConfigFile::EntryAttr); # implies Base
{
   %hAttribs =
       (
        Name    => ["c", "public",   "r",  undef],
        Value   => ["c", "public",   "rw", undef]
        );

}



# --- constructor ---------------------------------------------------
sub ConfigFile::Entry::new()
{
   my ($cPkgPI)   = shift;
   my $cNamePI    = shift;
   my $cValuePI   = shift;
   my $iSortNoPI  = shift;

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

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

   bless ($hSelf, $cClass);

   $hSelf->{"Name"}   = $cNamePI;
   $hSelf->{"Value"}  = $cValuePI;
   $hSelf->{"SortNo"} = $iSortNoPI;

   return $hSelf;
}


# --- Delete a ConfigFile-element ----------------------------------------------
sub ConfigFile::Entry::delete()
{
   my $hSelfPI    = shift;

   undef $hSelfPI;

   return undef;
}


# --- Append a ConfigFile-element ----------------------------------------------
sub ConfigFile::Entry::getValue()
{
   my $hSelfPI    = shift;
   my $cResult = $hSelfPI->{"Value"};

   # trim quotes in beginning or end of the line
   if ($cResult =~ /^"(.*)"$/)
   {
      $cResult = $1;
   }

   return ($cResult);
}



# --- Append a ConfigFile-element ----------------------------------------------
sub ConfigFile::Entry::isMultiLine()
{
   my $hSelfPI    = shift;

   return ($hSelfPI->{"Value"} =~ "\n");
}



1;


__END__


=head1 DESCRIPTION

=head1 The ConfigFile class

...

=head2 Note

=over 4

Currently the configfile is supported in the ini-file style.

=item B<sections>

In this style, the entries can be placed in sections (and
subsections). Sections are placed in B<[> and B<]> parens.
This looks like the following:

 [section]

or

 [Section subsection]

=item B<entries>

The single entries are described in the form B<entry = value>.
Single-lined entries as well as multiple-lined entries are
supported. They look like the following examples:

 entry = value

 entry = "value1 value2"

 entry = <<EOF
 value1
 value2
 value3
 EOF

=item B<comments>

Comments are always on a separate line. They start with a B<#> or B<;>.
Some examples:

 # This is a comment
 ; This is a comment too
 no_comment = # no comment


=back

=head2 Class Methods

=over 4

=item B<new>

 $oConfigFile     = ConfigFile->new([FillAtInsert => $bFill]);

...

=back

=head2 Class Attributes

=over 4

=item B<...>

...

=back
