1    package Pitonyak::ConfigFileParser;
   2    #************************************************************
   3    
   4    =head1 NAME
   5    
   6    Pitonyak::ConfigFileParser - Handle a configuration file.
   7    
   8    =head1 SYNOPSIS
   9    
  10    =begin html
  11    
  12    <p><code>
  13    use Pitonyak::ConfigFileParser;                                  <br/>
  14                                                           <br/>
  15    my $cfg = new Pitonyak::ConfigFileParser();                      <br/>
  16    $cfg->read_config_file('./modules/sample.cfg');        <br/>
  17    print 'keys = ('.join(', ', $cfg->get_keys()).")\n";   <br/>
  18    </code></p>
  19    
  20    =end html
  21    
  22    =head1 DESCRIPTION
  23    
  24    One might argue that this is nothing more than a light wrapper to read a
  25    configuration file into a hash. The C<< read_config_file method() >>
  26    is pretty smart at processing the file to increase ease of use.
  27    
  28    The configuration/properties file contains lines of the form:
  29    
  30    C<< <left hand side> = <right hand side> >>
  31    
  32    The following parsing rules are used:
  33    
  34    =over 4
  35    
  36    =item Blank lines are ignored.
  37    
  38    =item # is a comment character.
  39    
  40    =item Replace ${key} with the key value in the hash.
  41    
  42    =item The equal sign separates the keys from the values.
  43    
  44    =item leading and trailing space is removed.
  45    
  46    =item space around the equal sign is removed.
  47    
  48    =item Use a backslash as the escape character
  49    
  50    =back
  51    
  52    Use the escape character to insert special characters such as the comment, $,
  53    character, equal sign, leading or trailing space, or an escape character.
  54    Escaping characters with no special meaning, such as an 'a', evaluates to
  55    the character 'a'.
  56    
  57    You can prevent substitution of ${key} text by using \${key}.
  58    Substitution is done before escape characters are removed. So,the sequence
  59    ${\key} looks to see if there is a key named '\key' for replacement.
  60    
  61    Consider the following configuration:
  62    
  63    =begin html
  64    
  65    <p><code>
  66    file_base = ./files/                      <br/>
  67    partner = john                            <br/>
  68    ${partner}.loc = ${file_base}${partner}/  <br/>
  69    </code></p>
  70    
  71    =end html
  72    
  73    This is equivalent to
  74    
  75    =begin html
  76    
  77    <p><code>
  78    file_base = ./files/                      <br/>
  79    partner = john                            <br/>
  80    john.loc = ./files/john/                  <br/>
  81    </code></p>
  82    
  83    =end html
  84    
  85    =head1 Copyright
  86    
  87    Copyright 1998-2007 by Andrew Pitonyak
  88    
  89    More reworked code from Andrew's library. As with most of my
  90    code libraries, the code is free as free can be.
  91    
  92    =cut
  93    
  94    #************************************************************
  95    
  96    require Exporter;
  97    $VERSION = '1.02';
  98    @ISA     = qw(Exporter);
  99    @EXPORT  = qw(
 100    );
 101    
 102    @EXPORT_OK = qw(
 103      clear
 104      clear_key_value
 105      config_path
 106      config_name
 107      copy
 108      contains_key
 109      get_class_attribute
 110      get_config_full_name
 111      get_delimited_values
 112      get_keys
 113      get_hash_ref
 114      get_value
 115      get_value_default
 116      new
 117      read_config_file
 118      set_key_value
 119      set_logger
 120    );
 121    
 122    use Carp;
 123    use IO::File;
 124    use strict;
 125    use Pitonyak::DeepCopy qw(deep_copy);
 126    use Pitonyak::ADPLogger;
 127    use Pitonyak::StringUtil qw(trim_space);
 128    
 129    my %initial_attributes = (
 130        'is_ok'         => 1,    # Has an error occured?
 131        'config_name'   => 'configfile.cfg',
 132        'config_path'   => './',
 133    );
 134    
 135    #************************************************************
 136    
 137    =pod
 138    
 139    =head2 clear
 140    
 141    =over 4
 142    
 143    =item C<< $cfg->clear() >>
 144    
 145    Clear the entire configuration hash.
 146    
 147    =back
 148    
 149    =cut
 150    
 151    #************************************************************
 152    sub clear()
 153    {
 154      if ($#_ == 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 155      {
 156        # fastest way to clear a hash is to assign it to an empty list.
 157        ${$_[0]->{'config_hash'}} = ();
 158      }
 159    }
 160    
 161    #************************************************************
 162    
 163    =pod
 164    
 165    =head2 clear_key_value
 166    
 167    =over 4
 168    
 169    =item C<< $cfg->clear_key_value('key_text') >>
 170    
 171    Clear the specified key so that it is no longer in the configuration hash.
 172    
 173    =back
 174    
 175    =cut
 176    
 177    #************************************************************
 178    
 179    sub clear_key_value()
 180    {
 181      if ($#_ > 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 182      {
 183        udef( $_[0]->{'config_hash'}->{$_[1]});
 184      }
 185    }
 186    
 187    #************************************************************
 188    
 189    =pod
 190    
 191    =head2 config_name
 192    
 193    The config_name is the name of the configuration file with the file extension.
 194    The full file name is built by concatinating C<< config_path() >> and C<< config_name() >>.
 195    The extension is not assumed because it might be '.cfg' or '.properties'.
 196    
 197    =over 4
 198    
 199    =item C<< config_name() >>
 200    
 201    Return the default file name with file extension.
 202    
 203    =item C<< $cfg->config_name() >>
 204    
 205    Return the base log file name for the log object.
 206    
 207    =item C<< $cfg->config_name(file_name) >>
 208    
 209    Set the file name with extension, used for the next read.
 210    
 211    =back
 212    
 213    =cut
 214    
 215    #************************************************************
 216    
 217    sub config_name
 218    {
 219      return get_class_attribute( @_, 'config_name' );
 220    }
 221    
 222    #************************************************************
 223    
 224    =pod
 225    
 226    =head2 config_path
 227    
 228    The config_path identifies the directory containing the log file.
 229    The full file name is built by concatinating C<< config_path() >> and C<< config_name() >>.
 230    
 231    =over 4
 232    
 233    =item C<< config_path() >>
 234    
 235    Return the default path during initialization, which is './'.
 236    
 237    =item C<< $cfg->config_path() >>
 238    
 239    Return the the path to the next configuration file to read.
 240    Reading a configuration file with a fully specified file name
 241    does not cause the path or the file name to be set.
 242    
 243    =item C<< $cfg->config_path(path) >>
 244    
 245    Set the path for the configuration file, which will be used for the next read
 246    operation if the file name is not specified.
 247    If the provided path does not contain '/' or '\', then '/' is appended to
 248    to the path. The path itself is not checked for validity.
 249    
 250    If the provided path is an empty string, then the path is set to the
 251    default value.
 252    
 253    =back
 254    
 255    =cut
 256    
 257    #************************************************************
 258    
 259    sub config_path
 260    {
 261      # If zero arguments, return the initial value.
 262      return $initial_attributes{ 'config_path' } if $#_ < 0;
 263    
 264      # If one argument, assume the object is correct and return
 265      # the current path.
 266      return $_[0]->{ 'config_path' } if $#_ == 0;
 267    
 268      my $obj = shift;
 269      my $value = shift;
 270    
 271      if ($value =~ /[\/\\]$/)
 272      {
 273        $obj->{ 'config_path' } = $value;
 274      }
 275      elsif ($value eq '')
 276      {
 277        $obj->{ 'config_path' } = $initial_attributes{ 'config_path' };
 278      }
 279      else
 280      {
 281        $obj->{ 'config_path' } = $value.'/';
 282      }
 283    
 284      return $obj->{ 'config_path' };
 285    }
 286    
 287    #************************************************************
 288    
 289    =pod
 290    
 291    =head2 copy
 292    
 293    =over 4
 294    
 295    =item copy($config_object)
 296    
 297    Make a copy of one ConfigFileParser into another
 298    
 299    C<$obj1->copy($obj2)> is the same as C<$obj1 = $obj2>.
 300    The file handle is not copied and the
 301    the receiving ConfigFileParser is closed first.
 302    
 303    =back
 304    
 305    =cut
 306    
 307    #************************************************************
 308    
 309    sub copy
 310    {
 311      foreach my $key ( keys %initial_attributes )
 312      {
 313        if ($key eq 'logger')
 314        {
 315          # A normal copy is not sufficient.
 316          # I would need to literally generate a copy, but I
 317          # see no need to copy the logger.
 318        }
 319        elsif ( UNIVERSAL::isa( $_[1]->{$key}, 'HASH' ) )
 320        {
 321          # If I simply copy it over then we both reference the same thing!
 322          $_[0]->{$key} = deep_copy( $_[1]->{$key} );
 323        }
 324        else
 325        {
 326          $_[0]->{$key} = $_[1]->{$key};
 327        }
 328      }
 329    }
 330    
 331    #************************************************************
 332    
 333    =pod
 334    
 335    =head2 contains_key
 336    
 337    =over 4
 338    
 339    =item C<< $cfg->contains_key(key_name) >>
 340    
 341    Return 1 if the hash contains the key name and 0 otherwise.
 342    
 343    =back
 344    
 345    =cut
 346    
 347    #************************************************************
 348    
 349    sub contains_key()
 350    {
 351      if (($#_ > 0) && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 352      {
 353        return defined($_[0]->{'config_hash'}->{$_[1]}) ? 1 : 0;
 354      }
 355      carp('You must include a key name, such as $obj->contains_key("joe")');
 356      return 0;
 357    }
 358    
 359    #************************************************************
 360    
 361    =pod
 362    
 363    =head2 get_class_attribute
 364    
 365    The get_class_attribute method utilizes the fact that
 366    C<< $obj->method(@parms) >> is the same as
 367    C<< method($obj, @parms) >>. This method does not perform type checking
 368    to verify that this is true.
 369    
 370    The C<get_class_attribute> method is rarely called directly.
 371    
 372    =over 4
 373    
 374    =item C<< Pitonyak::ADPLogger::get_class_attribute($attribute_name) >>
 375    
 376    With only one paramter, the first parameter is
 377    assumed to be an attribute name and the default attribute value
 378    is returned.
 379    
 380    =item C<< $obj->get_class_attribute($attribute_name) >>
 381    
 382    With two arguments, the first is assumed
 383    to be a C<ADPLogger> object and the second is
 384    assumed to be an attribute name.
 385    The attribute value for the object is returned.
 386    
 387    
 388    =item C<< $obj->get_class_attribute($attribute_value, $attribute_name) >>
 389    
 390    With three arguments, the first is assumed to be the object,
 391    the second is a new attribute value, and the third is the attribute
 392    name to set. Although the order seems odd, this is intentional.
 393    
 394    Consider the method C<< is_ok >> defined as C<< return get_class_attribute( @_, 'is_ok' ); >>
 395    
 396    Remember that C<@_> refers to the argument list passed to the method. In all cases, the last argument
 397    passed to C<get_class_attribute> is the attribute name. If the method is called directly,
 398    this is the only argument.
 399    
 400    =back
 401    
 402    =cut
 403    
 404    #************************************************************
 405    
 406    sub get_class_attribute
 407    {
 408      return $initial_attributes{ $_[0] } if $#_ == 0;
 409      return $_[0]->{ $_[1] } if $#_ == 1;
 410      $_[0]->{ $_[2] } = $_[1];
 411      return $_[1];
 412    }
 413    
 414    #************************************************************
 415    
 416    =pod
 417    
 418    =head2 get_config_full_name
 419    
 420    =over 4
 421    
 422    =item C<< $cfg->get_config_full_name() >>
 423    
 424    Build and return the full path to the configuration file.
 425    Remember that C<< $cfg->config_path() >>
 426    returns a string with a trailing '/', so the value returned is equivalent to:
 427    
 428    C<< $cfg->config_path().$cfg->config_name() >>
 429    
 430    =back
 431    
 432    =cut
 433    
 434    #************************************************************
 435    
 436    sub get_config_full_name()
 437    {
 438      $_[0]->{'config_path'}.$_[0]->{'config_name'};
 439    }
 440    
 441    #************************************************************
 442    
 443    =pod
 444    
 445    =head2 get_hash_ref
 446    
 447    =over 4
 448    
 449    =item C<<  $cfg->get_hash_ref() >>
 450    
 451    Return a reference to the hash containing the properties.
 452    For example, to obtain the value for the key 'peter', you
 453    can use C<<  $cfg->get_hash_ref()->{'peter'} >> or
 454    C<<  $cfg->get_value('peter') >>.
 455    
 456    =back
 457    
 458    =cut
 459    
 460    #************************************************************
 461    sub get_hash_ref()
 462    {
 463      return $_[0]->{'config_hash'} if $#_ >= 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' );
 464    }
 465    
 466    #************************************************************
 467    
 468    =pod
 469    
 470    =head2 get_keys
 471    
 472    =over 4
 473    
 474    =item C<< $cfg->get_keys() >>
 475    
 476    Return the keys in the hash as an array.
 477    This is equivalent to
 478    C<<  keys(%{$cfg->get_hash_ref()}) >>.
 479    
 480    =back
 481    
 482    =cut
 483    
 484    #************************************************************
 485    
 486    sub get_keys()
 487    {
 488      return keys %{$_[0]->{'config_hash'}} if $#_ >= 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' );
 489    }
 490    
 491    #************************************************************
 492    
 493    =pod
 494    
 495    =head2 get_value
 496    
 497    =over 4
 498    
 499    =item C<< $cfg->get_value('key') >>
 500    
 501    Return the property value for the specified key.
 502    To obtain the value for the key 'peter', you
 503    can use C<<  $cfg->get_hash_ref()->{'peter'} >> or
 504    C<<  $cfg->get_value('peter') >>.
 505    
 506    =back
 507    
 508    =cut
 509    
 510    #************************************************************
 511    
 512    sub get_value
 513    {
 514      return $_[0]->{'config_hash'}->{$_[1]} if $#_ > 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' );
 515    }
 516    
 517    #************************************************************
 518    
 519    =pod
 520    
 521    =head2 get_value_default
 522    
 523    =over 4
 524    
 525    =item C<< $cfg->get_value_default('key') >>
 526    
 527    This version is identical to C<< $cfg->get_value_default('key') >>,
 528    except that it returns an empty string if the key does not exist.
 529    
 530    =item C<< $cfg->get_value_default('key', 'default') >>
 531    
 532    If the property exists, return the value. If the property
 533    does not exist, return the specified default value.
 534    
 535    =back
 536    
 537    =cut
 538    
 539    #************************************************************
 540    
 541    sub get_value_default
 542    {
 543        if ( $#_ > 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 544        {
 545            return $_[0]->{'config_hash'}->{$_[1]} if defined($_[0]->{'config_hash'}->{$_[1]});
 546        }
 547        return ($#_ > 1) ? $_[2] : '';
 548    }
 549    
 550    
 551    #************************************************************
 552    
 553    =pod
 554    
 555    =head2 get_delimited_values
 556    
 557    =over 4
 558    
 559    =item C<< $cfg->get_delimited_values('key') >>
 560    
 561    Omitting the delimiter is the same as calling
 562    C<<  $cfg->get_delimited_values('key', ',') >>.
 563    
 564    =item C<< $cfg->get_delimited_values('key', 'delimiter') >>
 565    
 566    Extract the specified key from the configuration item.
 567    Assume that the key contains a list of items delimited with the
 568    specified delimiter.
 569    Leading and trailing spaces are removed.
 570    All of the values are returned as an array.
 571    
 572    =back
 573    
 574    =cut
 575    
 576    #************************************************************
 577    
 578    sub get_delimited_values
 579    {
 580        my @array;
 581        if ($#_ > 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 582        {
 583            my $obj = shift;
 584            my $name = shift;
 585            my $delim = ',';
 586            $delim = shift if $#_ >= 0;
 587            return @array if not defined($obj->{'config_hash'}->{$name});
 588            return map {trim_space($_)} split(',', $obj->{'config_hash'}->{$name});
 589        }
 590        return @array;
 591    }
 592    
 593    #************************************************************
 594    
 595    =pod
 596    
 597    =head2 new
 598    
 599    =over 4
 600    
 601    =item C<< $cfg_copy = $cfg->new() >>
 602    
 603    Generate a new copy of a configuration object.
 604    
 605    =back
 606    
 607    =cut
 608    
 609    #************************************************************
 610    
 611    sub new
 612    {
 613      my $self = shift;
 614      my $objref = bless {}, ref($self) || $self;
 615      $objref->initialize();
 616      if ( ref($self) )
 617      {
 618        $objref->copy($self);
 619      }
 620      return $objref;
 621    }
 622    
 623    #************************************************************
 624    
 625    =pod
 626    
 627    =head2 read_config_file
 628    
 629    Read a config/properties file from disk and populate this object.
 630    The current object is cleared reading the file.
 631    Any current values are over-written.
 632    
 633    =over 4
 634    
 635    =item C<< $cfg->read_config_file() >>
 636    
 637    The directory
 638    and name must be set using C<config_path()> and C<config_name()>.
 639    C<get_config_full_name()> is used to build the full path.
 640    
 641    =item C<< $cfg->read_config_file('full_path_to_file') >>
 642    
 643    Neither C<config_path()> nor C<config_name()> are updated.
 644    
 645    =back
 646    
 647    =cut
 648    
 649    #************************************************************
 650    
 651    sub read_config_file()
 652    {
 653      if ( $#_ < 0 || !UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 654      {
 655        carp('Usage: obj->read_config_file() or obj->read_config_file(<config_file_name>)');
 656        return undef;
 657      }
 658      my $log = $_[0]->{'config_hash'}->{'logger'} if defined( $_[0]->{'config_hash'}->{'logger'} );
 659    
 660      # Either build the full name, or take it from the argument.
 661      my $file_name = ($#_ > 0) ? $_[1] : $_[0]->get_config_full_name();
 662    
 663      # First, read the entire file into an array.
 664      # Remove comments, blank lines, as well as leading and trailing white space.
 665      my $rc = 0;
 666      my $line = 0;
 667      my $handle = new IO::File;
 668      my ($key, $value);
 669      if ( not $handle->open( '<' . $file_name ) )
 670      {
 671        carp("Unable to open configuration file $file_name because $!");
 672        return undef;
 673      }
 674      else
 675      {
 676        while (<$handle>)
 677        {
 678          $key = '';
 679          $value = '';
 680          ++$line;
 681          chomp;
 682          s/^\s*//;           # leading spaces
 683          #
 684          # This one is more difficult....
 685          # s/(.*?(?<!\\)(?:(\\\\)*))#.*/$1/
 686          #
 687          # .*?           include all preceding text minimally
 688          # (?<!\\)       Do not allow a preceding backslash
 689          # (?:(\\\\)*))  Match any groups of two backslashes
 690          # #.*           Match a # followed by anything
 691          #
 692          # This effectively considers the first # not preceded by
 693          # an odd number of \ to be a comment.
 694          #
 695          if ($] >= 5.005)
 696          {
 697            s/(.*?(?<!\\)(?:(\\\\)*))#.*/$1/;
 698            s/(?<!\\)\s*$//;           # Trailing spaces not preceded by a \
 699    
 700            # Look for substition values of the form ${key_name}.
 701            # Replace these values with entries that have already been read.
 702            while (/(?<!\\)\${(.*?)}/)
 703            {
 704              my $subst_name = $1;
 705              my $subst_value = defined($_[0]->{'config_hash'}->{$subst_name}) ? $_[0]->{'config_hash'}->{$subst_name} : '';
 706              $log->trace("Config line $line: Replace $subst_name with $subst_value") if defined ($log);
 707              s/(?<!\\)(\${.*?})/$subst_value/;
 708            }
 709    
 710            if ($_ ne '')              # Ignore empty lines
 711            {
 712              # Ignore lines that do not contain '='
 713              if (/(.*?(?<!\\)(?:(\\\\)*))=(.*)$/)
 714              {
 715                $key = $1;
 716                $value = $3;
 717                $key =~ s/(?<!\\)\s*$//; # Trailing spaces not preceded by a \
 718                $value =~ s/^\s*//;      # Leading spaces on the value.
 719                $key   =~ s/\\(.)/$1/g;  # Now remove \ chars
 720                $value =~ s/\\(.)/$1/g;  # Now remove \ chars
 721    
 722                # Although it might be argued that this is not the best time for this,
 723                # property substitution is done now.
 724    
 725                $_[0]->{'config_hash'}->{$key} = $value;
 726                $log->trace("Config line $line: ($key)=($value)") if defined ($log);
 727              }
 728              else
 729              {
 730                my $error_msg = "Line $line does not contain the '=' character";
 731                carp($error_msg);
 732                $log->trace($error_msg) if defined ($log);
 733              }
 734            }
 735          }
 736          else
 737          {
 738            my $error_msg = 'Please use a version of perl newer than 5.004';
 739            carp($error_msg);
 740            $log->trace($error_msg) if defined ($log);
 741            return undef;
 742            s/(.*?)#.*/$1/;     # ?? This is WRONG but supported by perl 5.004
 743            s/\s*$//;           # ?? This is WRONG but supported by perl 5.004
 744          }
 745          #s/\\(.)/$1/g;              # Now remove \ chars
 746        }
 747        $handle->close();
 748      }
 749      return $rc;
 750    }
 751    
 752    #************************************************************
 753    
 754    =pod
 755    
 756    =head2 set_key_value
 757    
 758    =over 4
 759    
 760    =item C<< $cfg->set_key_value(key, value) >>
 761    
 762    Set the specified key to the specified value.
 763    
 764    =back
 765    
 766    =cut
 767    
 768    #************************************************************
 769    sub set_key_value()
 770    {
 771      return $_[0]->{'config_hash'}->{$_[1]} = $_[2] if $#_ > 1 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' );
 772    }
 773    
 774    #************************************************************
 775    
 776    =pod
 777    
 778    =head2 set_logger
 779    
 780    =over 4
 781    
 782    =item C<< $cfg->set_logger($log) >>
 783    
 784    Add a logger to the ConfigFileParser. If the logger is present, it is
 785    used to report errors and trace information.
 786    
 787    =back
 788    
 789    =cut
 790    
 791    #************************************************************
 792    
 793    sub set_logger()
 794    {
 795      if ($#_ >= 0 && UNIVERSAL::isa( $_[0], 'ConfigFileParser' ))
 796      {
 797        if ($#_ > 0)
 798        {
 799          if ( UNIVERSAL::isa( $_[1], 'ADPLogger' ) )
 800          {
 801            $_[0]->{'config_hash'}->{'logger'} = $_[1];
 802          }
 803          else
 804          {
 805            carp('The argument to set_logger must be of type Logger');
 806          }
 807        }
 808        else
 809        {
 810          udef( $_[0]->{'config_hash'}->{'logger'} );
 811    
 812        }
 813      }
 814    }
 815    
 816    #************************************************************
 817    
 818    =pod
 819    
 820    =head1 Private Methods
 821    
 822    =head2 initialize
 823    
 824    =over 4
 825    
 826    =item C<< initialize() >>
 827    
 828    The C<< initialize() >> method is called automatically when an object is created.
 829    The new method also calls C<< initialize() >> directly
 830    
 831    Initialize the data structure by copying values from the initial attributes hash
 832    into the newly created object. Finally, set the read properties hash to
 833    an empty reference.
 834    
 835    =back
 836    
 837    =cut
 838    
 839    #************************************************************
 840    
 841    sub initialize
 842    {
 843      foreach my $key ( keys %initial_attributes )
 844      {
 845        if ( UNIVERSAL::isa( $initial_attributes{$key}, 'HASH' ) )
 846        {
 847          # If I simply copy it over then we both reference the same thing!
 848          $_[0]->{$key} = deep_copy( $initial_attributes{$key} );
 849        }
 850        else
 851        {
 852          $_[0]->{$key} = $initial_attributes{$key};
 853        }
 854      }
 855    
 856      # Set the initial reference to the configuration hash (will hold file values).
 857      $_[0]->{'config_hash'} = {};
 858    }
 859    
 860    #************************************************************
 861    #**                                                        **
 862    #**  Input : None.                                         **
 863    #**                                                        **
 864    #**  Output: None.                                         **
 865    #**                                                        **
 866    #**  Notes : Nothing to destroy or close, but just in case.**
 867    #**                                                        **
 868    #************************************************************
 869    
 870    sub DESTROY
 871    {
 872    }
 873    
 874    1;