package Pitonyak::ConfigFileParser; #************************************************************ =head1 NAME Pitonyak::ConfigFileParser - Handle a configuration file. =head1 SYNOPSIS =begin html

use Pitonyak::ConfigFileParser;

my $cfg = new Pitonyak::ConfigFileParser();
$cfg->read_config_file('./modules/sample.cfg');
print 'keys = ('.join(', ', $cfg->get_keys()).")\n";

=end html =head1 DESCRIPTION One might argue that this is nothing more than a light wrapper to read a configuration file into a hash. The C<< read_config_file method() >> is pretty smart at processing the file to increase ease of use. The configuration/properties file contains lines of the form: C<< = >> The following parsing rules are used: =over 4 =item Blank lines are ignored. =item # is a comment character. =item Replace ${key} with the key value in the hash. =item The equal sign separates the keys from the values. =item leading and trailing space is removed. =item space around the equal sign is removed. =item Use a backslash as the escape character =back Use the escape character to insert special characters such as the comment, $, character, equal sign, leading or trailing space, or an escape character. Escaping characters with no special meaning, such as an 'a', evaluates to the character 'a'. You can prevent substitution of ${key} text by using \${key}. Substitution is done before escape characters are removed. So,the sequence ${\key} looks to see if there is a key named '\key' for replacement. Consider the following configuration: =begin html

file_base = ./files/
partner = john
${partner}.loc = ${file_base}${partner}/

=end html This is equivalent to =begin html

file_base = ./files/
partner = john
john.loc = ./files/john/

=end html =head1 Copyright Copyright 1998-2007 by Andrew Pitonyak More reworked code from Andrew's library. As with most of my code libraries, the code is free as free can be. =cut #************************************************************ require Exporter; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); $VERSION = '1.02'; @ISA = qw(Exporter); @EXPORT = qw( ); @EXPORT_OK = qw( clear clear_key_value config_path config_name copy contains_key get_class_attribute get_config_full_name get_delimited_values get_keys get_hash_ref get_value get_value_default new read_config_file set_key_value set_logger ); use Carp; use IO::File; use strict; use Pitonyak::DeepCopy qw(deep_copy); use Pitonyak::ADPLogger; use Pitonyak::StringUtil qw(trim_space); my %initial_attributes = ( 'is_ok' => 1, # Has an error occured? 'config_name' => 'configfile.cfg', 'config_path' => './', ); #************************************************************ =pod =head2 clear =over 4 =item C<< $cfg->clear() >> Clear the entire configuration hash. =back =cut #************************************************************ sub clear() { if ($#_ == 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { # fastest way to clear a hash is to assign it to an empty list. ${$_[0]->{'config_hash'}} = (); } } #************************************************************ =pod =head2 clear_key_value =over 4 =item C<< $cfg->clear_key_value('key_text') >> Clear the specified key so that it is no longer in the configuration hash. =back =cut #************************************************************ sub clear_key_value() { if ($#_ > 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { udef( $_[0]->{'config_hash'}->{$_[1]}); } } #************************************************************ =pod =head2 config_name The config_name is the name of the configuration file with the file extension. The full file name is built by concatinating C<< config_path() >> and C<< config_name() >>. The extension is not assumed because it might be '.cfg' or '.properties'. =over 4 =item C<< config_name() >> Return the default file name with file extension. =item C<< $cfg->config_name() >> Return the base log file name for the log object. =item C<< $cfg->config_name(file_name) >> Set the file name with extension, used for the next read. =back =cut #************************************************************ sub config_name { return get_class_attribute( @_, 'config_name' ); } #************************************************************ =pod =head2 config_path The config_path identifies the directory containing the log file. The full file name is built by concatinating C<< config_path() >> and C<< config_name() >>. =over 4 =item C<< config_path() >> Return the default path during initialization, which is './'. =item C<< $cfg->config_path() >> Return the the path to the next configuration file to read. Reading a configuration file with a fully specified file name does not cause the path or the file name to be set. =item C<< $cfg->config_path(path) >> Set the path for the configuration file, which will be used for the next read operation if the file name is not specified. If the provided path does not contain '/' or '\', then '/' is appended to to the path. The path itself is not checked for validity. If the provided path is an empty string, then the path is set to the default value. =back =cut #************************************************************ sub config_path { # If zero arguments, return the initial value. return $initial_attributes{ 'config_path' } if $#_ < 0; # If one argument, assume the object is correct and return # the current path. return $_[0]->{ 'config_path' } if $#_ == 0; my $obj = shift; my $value = shift; if ($value =~ /[\/\\]$/) { $obj->{ 'config_path' } = $value; } elsif ($value eq '') { $obj->{ 'config_path' } = $initial_attributes{ 'config_path' }; } else { $obj->{ 'config_path' } = $value.'/'; } return $obj->{ 'config_path' }; } #************************************************************ =pod =head2 copy =over 4 =item copy($config_object) Make a copy of one ConfigFileParser into another C<$obj1->copy($obj2)> is the same as C<$obj1 = $obj2>. The file handle is not copied and the the receiving ConfigFileParser is closed first. =back =cut #************************************************************ sub copy { foreach my $key ( keys %initial_attributes ) { if ($key eq 'logger') { # A normal copy is not sufficient. # I would need to literally generate a copy, but I # see no need to copy the logger. } elsif ( UNIVERSAL::isa( $_[1]->{$key}, 'HASH' ) ) { # If I simply copy it over then we both reference the same thing! $_[0]->{$key} = deep_copy( $_[1]->{$key} ); } else { $_[0]->{$key} = $_[1]->{$key}; } } } #************************************************************ =pod =head2 contains_key =over 4 =item C<< $cfg->contains_key(key_name) >> Return 1 if the hash contains the key name and 0 otherwise. =back =cut #************************************************************ sub contains_key() { if (($#_ > 0) && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { return defined($_[0]->{'config_hash'}->{$_[1]}) ? 1 : 0; } carp('You must include a key name, such as $obj->contains_key("joe")'); return 0; } #************************************************************ =pod =head2 get_class_attribute The get_class_attribute method utilizes the fact that C<< $obj->method(@parms) >> is the same as C<< method($obj, @parms) >>. This method does not perform type checking to verify that this is true. The C method is rarely called directly. =over 4 =item C<< Pitonyak::ADPLogger::get_class_attribute($attribute_name) >> With only one paramter, the first parameter is assumed to be an attribute name and the default attribute value is returned. =item C<< $obj->get_class_attribute($attribute_name) >> With two arguments, the first is assumed to be a C object and the second is assumed to be an attribute name. The attribute value for the object is returned. =item C<< $obj->get_class_attribute($attribute_value, $attribute_name) >> With three arguments, the first is assumed to be the object, the second is a new attribute value, and the third is the attribute name to set. Although the order seems odd, this is intentional. Consider the method C<< is_ok >> defined as C<< return get_class_attribute( @_, 'is_ok' ); >> Remember that C<@_> refers to the argument list passed to the method. In all cases, the last argument passed to C is the attribute name. If the method is called directly, this is the only argument. =back =cut #************************************************************ sub get_class_attribute { return $initial_attributes{ $_[0] } if $#_ == 0; return $_[0]->{ $_[1] } if $#_ == 1; $_[0]->{ $_[2] } = $_[1]; return $_[1]; } #************************************************************ =pod =head2 get_config_full_name =over 4 =item C<< $cfg->get_config_full_name() >> Build and return the full path to the configuration file. Remember that C<< $cfg->config_path() >> returns a string with a trailing '/', so the value returned is equivalent to: C<< $cfg->config_path().$cfg->config_name() >> =back =cut #************************************************************ sub get_config_full_name() { $_[0]->{'config_path'}.$_[0]->{'config_name'}; } #************************************************************ =pod =head2 get_hash_ref =over 4 =item C<< $cfg->get_hash_ref() >> Return a reference to the hash containing the properties. For example, to obtain the value for the key 'peter', you can use C<< $cfg->get_hash_ref()->{'peter'} >> or C<< $cfg->get_value('peter') >>. =back =cut #************************************************************ sub get_hash_ref() { return $_[0]->{'config_hash'} if $#_ >= 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' ); } #************************************************************ =pod =head2 get_keys =over 4 =item C<< $cfg->get_keys() >> Return the keys in the hash as an array. This is equivalent to C<< keys(%{$cfg->get_hash_ref()}) >>. =back =cut #************************************************************ sub get_keys() { return keys %{$_[0]->{'config_hash'}} if $#_ >= 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' ); } #************************************************************ =pod =head2 get_value =over 4 =item C<< $cfg->get_value('key') >> Return the property value for the specified key. To obtain the value for the key 'peter', you can use C<< $cfg->get_hash_ref()->{'peter'} >> or C<< $cfg->get_value('peter') >>. =back =cut #************************************************************ sub get_value { return $_[0]->{'config_hash'}->{$_[1]} if $#_ > 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' ); } #************************************************************ =pod =head2 get_value_default =over 4 =item C<< $cfg->get_value_default('key') >> This version is identical to C<< $cfg->get_value_default('key') >>, except that it returns an empty string if the key does not exist. =item C<< $cfg->get_value_default('key', 'default') >> If the property exists, return the value. If the property does not exist, return the specified default value. =back =cut #************************************************************ sub get_value_default { if ( $#_ > 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { return $_[0]->{'config_hash'}->{$_[1]} if defined($_[0]->{'config_hash'}->{$_[1]}); } return ($#_ > 1) ? $_[2] : ''; } #************************************************************ =pod =head2 get_delimited_values =over 4 =item C<< $cfg->get_delimited_values('key') >> Omitting the delimiter is the same as calling C<< $cfg->get_delimited_values('key', ',') >>. =item C<< $cfg->get_delimited_values('key', 'delimiter') >> Extract the specified key from the configuration item. Assume that the key contains a list of items delimited with the specified delimiter. Leading and trailing spaces are removed. All of the values are returned as an array. =back =cut #************************************************************ sub get_delimited_values { my @array; if ($#_ > 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { my $obj = shift; my $name = shift; my $delim = ','; $delim = shift if $#_ >= 0; return @array if not defined($obj->{'config_hash'}->{$name}); return map {trim_space($_)} split($delim, $obj->{'config_hash'}->{$name}); } return @array; } #************************************************************ =pod =head2 new =over 4 =item C<< $cfg_copy = $cfg->new() >> Generate a new copy of a configuration object. =back =cut #************************************************************ sub new { my $self = shift; my $objref = bless {}, ref($self) || $self; $objref->initialize(); if ( ref($self) ) { $objref->copy($self); } return $objref; } #************************************************************ =pod =head2 read_config_file Read a config/properties file from disk and populate this object. The current object is cleared reading the file. Any current values are over-written. =over 4 =item C<< $cfg->read_config_file() >> The directory and name must be set using C and C. C is used to build the full path. =item C<< $cfg->read_config_file('full_path_to_file') >> Neither C nor C are updated. =back =cut #************************************************************ sub read_config_file() { if ( $#_ < 0 || !UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { carp('Usage: obj->read_config_file() or obj->read_config_file()'); return undef; } my $log = $_[0]->{'config_hash'}->{'logger'} if defined( $_[0]->{'config_hash'}->{'logger'} ); # Either build the full name, or take it from the argument. my $file_name = ($#_ > 0) ? $_[1] : $_[0]->get_config_full_name(); # First, read the entire file into an array. # Remove comments, blank lines, as well as leading and trailing white space. my $rc = 0; my $line = 0; my $handle = new IO::File; my ($key, $value); if ( not $handle->open( '<' . $file_name ) ) { carp("Unable to open configuration file $file_name because $!"); return undef; } else { while (<$handle>) { $key = ''; $value = ''; ++$line; chomp; s/^\s*//; # leading spaces # # This one is more difficult.... # s/(.*?(?= 5.005) { s/(.*?(?{'config_hash'}->{$subst_name}) ? $_[0]->{'config_hash'}->{$subst_name} : ''; $log->trace("Config line $line: Replace $subst_name with $subst_value") if defined ($log); s/(?{'config_hash'}->{$key} = $value; $log->trace("Config line $line: ($key)=($value)") if defined ($log); } else { my $error_msg = "Line $line does not contain the '=' character"; carp($error_msg); $log->trace($error_msg) if defined ($log); } } } else { my $error_msg = 'Please use a version of perl newer than 5.004'; carp($error_msg); $log->trace($error_msg) if defined ($log); return undef; s/(.*?)#.*/$1/; # ?? This is WRONG but supported by perl 5.004 s/\s*$//; # ?? This is WRONG but supported by perl 5.004 } #s/\\(.)/$1/g; # Now remove \ chars } $handle->close(); } return $rc; } #************************************************************ =pod =head2 set_key_value =over 4 =item C<< $cfg->set_key_value(key, value) >> Set the specified key to the specified value. =back =cut #************************************************************ sub set_key_value() { return $_[0]->{'config_hash'}->{$_[1]} = $_[2] if $#_ > 1 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' ); } #************************************************************ =pod =head2 set_logger =over 4 =item C<< $cfg->set_logger($log) >> Add a logger to the ConfigFileParser. If the logger is present, it is used to report errors and trace information. =back =cut #************************************************************ sub set_logger() { if ($#_ >= 0 && UNIVERSAL::isa( $_[0], 'Pitonyak::ConfigFileParser' )) { if ($#_ > 0) { if ( UNIVERSAL::isa( $_[1], 'Pitonyak::ADPLogger' ) ) { $_[0]->{'config_hash'}->{'logger'} = $_[1]; } else { carp('The argument to set_logger must be of type Logger'); } } else { udef( $_[0]->{'config_hash'}->{'logger'} ); } } } #************************************************************ =pod =head1 Private Methods =head2 initialize =over 4 =item C<< initialize() >> The C<< initialize() >> method is called automatically when an object is created. The new method also calls C<< initialize() >> directly Initialize the data structure by copying values from the initial attributes hash into the newly created object. Finally, set the read properties hash to an empty reference. =back =cut #************************************************************ sub initialize { foreach my $key ( keys %initial_attributes ) { if ( UNIVERSAL::isa( $initial_attributes{$key}, 'HASH' ) ) { # If I simply copy it over then we both reference the same thing! $_[0]->{$key} = deep_copy( $initial_attributes{$key} ); } else { $_[0]->{$key} = $initial_attributes{$key}; } } # Set the initial reference to the configuration hash (will hold file values). $_[0]->{'config_hash'} = {}; } #************************************************************ #** ** #** Input : None. ** #** ** #** Output: None. ** #** ** #** Notes : Nothing to destroy or close, but just in case.** #** ** #************************************************************ sub DESTROY { } 1;