ca400f7ac759d3b20d2f1b7a4a8c3e72a842b86b
[OpenSRF.git] / src / perl / lib / OpenSRF / Utils / Config.pm
1 package OpenSRF::Utils::Config::Section;
2
3 no strict 'refs';
4
5 use vars qw/@ISA $AUTOLOAD $VERSION/;
6 push @ISA, qw/OpenSRF::Utils/;
7
8 use OpenSRF::Utils (':common');
9 use Net::Domain qw/hostfqdn/;
10
11 $VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
12
13 my %SECTIONCACHE;
14 my %SUBSECTION_FIXUP;
15
16 #use overload '""' => \&OpenSRF::Utils::Config::dump_ini;
17
18 sub SECTION {
19         my $sec = shift;
20         return $sec->__id(@_);
21 }
22
23 sub new {
24         my $self = shift;
25         my $class = ref($self) || $self;
26
27         $self = bless {}, $class;
28
29         $self->_sub_builder('__id');
30         # Hard-code this to match old bootstrap.conf section name
31         $self->__id('bootstrap');
32
33         my $bootstrap = shift;
34
35         foreach my $key (sort keys %$bootstrap) {
36                 $self->_sub_builder($key);
37                 $self->$key($bootstrap->{$key});
38         }
39
40         return $self;
41 }
42
43 package OpenSRF::Utils::Config;
44
45 use vars qw/@ISA $AUTOLOAD $VERSION $OpenSRF::Utils::ConfigCache/;
46 push @ISA, qw/OpenSRF::Utils/;
47
48 use FileHandle;
49 use XML::LibXML;
50 use OpenSRF::Utils (':common');  
51 use OpenSRF::Utils::Logger;
52 use Net::Domain qw/hostfqdn/;
53
54 #use overload '""' => \&OpenSRF::Utils::Config::dump_ini;
55
56 sub import {
57         my $class = shift;
58         my $config_file = shift;
59
60         return unless $config_file;
61
62         $class->load( config_file => $config_file);
63 }
64
65 sub dump_ini {
66         no warnings;
67         my $self = shift;
68         my $string;
69         my $included = 0;
70         if ($self->isa('OpenSRF::Utils::Config')) {
71                 if (UNIVERSAL::isa(scalar(caller()), 'OpenSRF::Utils::Config' )) {
72                         $included = 1;
73                 } else {
74                         $string = "# Main File:  " . $self->FILE . "\n\n" . $string;
75                 }
76         }
77         for my $section ( ('__id', grep { $_ ne '__id' } sort keys %$self) ) {
78                 next if ($section eq 'env' && $self->isa('OpenSRF::Utils::Config'));
79                 if ($section eq '__id') {
80                         $string .= '['.$self->SECTION."]\n" if ($self->isa('OpenSRF::Utils::Config::Section'));
81                 } elsif (ref($self->$section)) {
82                         if (ref($self->$section) =~ /ARRAY/o) {
83                                 $string .= "list:$section = ". join(', ', @{$self->$section}) . "\n";
84                         } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config::Section')) {
85                                 if ($self->isa('OpenSRF::Utils::Config::Section')) {
86                                         $string .= "subsection:$section = " . $self->$section->SECTION . "\n";
87                                         next;
88                                 } else {
89                                         next if ($self->$section->{__sub} && !$included);
90                                         $string .= $self->$section . "\n";
91                                 }
92                         } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config')) {
93                                 $string .= $self->$section . "\n";
94                         }
95                 } else {
96                         next if $section eq '__sub';
97                         $string .= "$section = " . $self->$section . "\n";
98                 }
99         }
100         if ($included) {
101                 $string =~ s/^/## /gm;
102                 $string = "# Subfile:  " . $self->FILE . "\n#" . '-'x79 . "\n".'#include "'.$self->FILE."\"\n". $string;
103         }
104
105         return $string;
106 }
107
108 =head1 NAME
109  
110 OpenSRF::Utils::Config
111  
112
113 =head1 SYNOPSIS
114
115   use OpenSRF::Utils::Config;
116
117   my $config_obj = OpenSRF::Utils::Config->load( config_file   => '/config/file.cnf' );
118
119   my $attrs_href = $config_obj->bootstrap();
120
121   $config_obj->bootstrap->loglevel(0);
122
123   open FH, '>'.$config_obj->FILE() . '.new';
124   print FH $config_obj;
125   close FH;
126
127 =head1 DESCRIPTION
128
129 This module is mainly used by other OpenSRF modules to load an OpenSRF
130 configuration file.  OpenSRF configuration files are XML files that
131 contain a C<< <config> >> root element and an C<< <opensrf> >> child
132 element (in XPath notation, C</config/opensrf/>). Each child element
133 is converted into a hash key=>value pair. Elements that contain other
134 XML elements are pushed into arrays and added as an array reference to
135 the hash. Scalar values have whitespace trimmed from the left and
136 right sides.
137
138 Child elements of C<< <config> >> other than C<< <opensrf> >> are
139 currently ignored by this module.
140
141 =head1 EXAMPLE
142
143 Given an OpenSRF configuration file named F<opensrf_core.xml> with the
144 following content:
145
146   <?xml version='1.0'?>
147   <config>
148     <opensrf>
149       <router_name>router</router_name>
150
151       <routers> 
152         <router>localhost</router>
153         <router>otherhost</router>
154       </routers>
155
156       <logfile>/var/log/osrfsys.log</logfile>
157     </opensrf>
158   </config>
159
160 ... calling C<< OpenSRF::Utils::Config->load(config_file =>
161 'opensrf_core.xml') >> will create a hash with the following
162 structure:
163
164   {
165     router_name => 'router',
166     routers => ['localhost', 'otherhost'],
167     logfile => '/var/log/osrfsys.log'
168   }
169
170 You can retrieve any of these values by name from the bootstrap
171 section of C<$config_obj>; for example:
172
173   $config_obj->bootstrap->router_name
174
175 =head1 NOTES
176
177 For compatibility with a previous version of OpenSRF configuration
178 files, the F</config/opensrf/> section has a hardcoded name of
179 B<bootstrap>. However, future iterations of this module may extend the
180 ability of the module to parse the entire OpenSRF configuration file
181 and provide sections named after the sibling elements of
182 C</config/opensrf>.
183
184 Hashrefs of sections can be returned by calling a method of the object
185 of the same name as the section.  They can be set by passing a hashref
186 back to the same method.  Sections will B<NOT> be autovivicated,
187 though.
188
189
190 =head1 METHODS
191
192
193 =cut
194
195
196 $VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
197
198
199 =head2 OpenSRF::Utils::Config->load( config_file => '/some/config/file.cnf' )
200
201 Returns a OpenSRF::Utils::Config object representing the config file
202 that was loaded.  The most recently loaded config file (hopefully the
203 only one per app) is stored at $OpenSRF::Utils::ConfigCache. Use
204 OpenSRF::Utils::Config::current() to get at it.
205
206 =cut
207
208 sub load {
209         my $pkg = shift;
210         $pkg = ref($pkg) || $pkg;
211
212         my %args = @_;
213
214         (my $new_pkg = $args{config_file}) =~ s/\W+/_/g;
215         $new_pkg .= "::$pkg";
216         $new_section_pkg .= "${new_pkg}::Section";
217
218         {       eval <<"                PERL";
219
220                         package $new_pkg;
221                         use base $pkg;
222                         sub section_pkg { return '$new_section_pkg'; }
223
224                         package $new_section_pkg;
225                         use base "${pkg}::Section";
226         
227                 PERL
228         }
229
230         return $new_pkg->_load( %args );
231 }
232
233 sub _load {
234         my $pkg = shift;
235         $pkg = ref($pkg) || $pkg;
236         my $self = {@_};
237         bless $self, $pkg;
238
239         no warnings;
240         if ((exists $$self{config_file} and OpenSRF::Utils::Config->current) and (OpenSRF::Utils::Config->current->FILE eq $$self{config_file}) and (!$self->{force})) {
241                 delete $$self{force};
242                 return OpenSRF::Utils::Config->current();
243         }
244
245         $self->_sub_builder('__id');
246         $self->FILE($$self{config_file});
247         delete $$self{config_file};
248         return undef unless ($self->FILE);
249
250         $self->load_config();
251         $self->load_env();
252         $self->mangle_dirs();
253         $self->mangle_logs();
254
255         $OpenSRF::Utils::ConfigCache = $self unless $self->nocache;
256         delete $$self{nocache};
257         delete $$self{force};
258         return $self;
259 }
260
261 sub sections {
262         my $self = shift;
263         my %filters = @_;
264
265         my @parts = (grep { UNIVERSAL::isa($_,'OpenSRF::Utils::Config::Section') } values %$self);
266         if (keys %filters) {
267                 my $must_match = scalar(keys %filters);
268                 my @ok_parts;
269                 foreach my $part (@parts) {
270                         my $part_count = 0;
271                         for my $fkey (keys %filters) {
272                                 $part_count++ if ($part->$key eq $filters{$key});
273                         }
274                         push @ok_parts, $part if ($part_count == $must_match);
275                 }
276                 return @ok_parts;
277         }
278         return @parts;
279 }
280
281 sub current {
282         return $OpenSRF::Utils::ConfigCache;
283 }
284
285 sub FILE {
286         return shift()->__id(@_);
287 }
288
289 sub load_env {
290         my $self = shift;
291         my $host = $ENV{'OSRF_HOSTNAME'} || hostfqdn();
292         chomp $host;
293         $$self{env} = $self->section_pkg->new;
294         $$self{env}{hostname} = $host;
295 }
296
297 sub mangle_logs {
298         my $self = shift;
299         return unless ($self->logs && $self->dirs && $self->dirs->log_dir);
300         for my $i ( keys %{$self->logs} ) {
301                 next if ($self->logs->$i =~ /^\//);
302                 $self->logs->$i($self->dirs->log_dir."/".$self->logs->$i);
303         }
304 }
305
306 sub mangle_dirs {
307         my $self = shift;
308         return unless ($self->dirs && $self->dirs->base_dir);
309         for my $i ( keys %{$self->dirs} ) {
310                 if ( $i ne 'base_dir' ) {
311                         next if ($self->dirs->$i =~ /^\//);
312                         my $dir_tmp = $self->dirs->base_dir."/".$self->dirs->$i;
313                         $dir_tmp =~ s#//#/#go;
314                         $dir_tmp =~ s#/$##go;
315                         $self->dirs->$i($dir_tmp);
316                 }
317         }
318 }
319
320 sub load_config {
321         my $self = shift;
322         my $parser = XML::LibXML->new();
323
324         # Hash of config values
325         my %bootstrap;
326         
327         # Return an XML::LibXML::Document object
328         my $config = $parser->parse_file($self->FILE);
329
330         unless ($config) {
331                 OpenSRF::Utils::Logger->error("Could not open ".$self->FILE.": $!\n");
332                 die "Could not open ".$self->FILE.": $!\n";
333         }
334
335         # Return an XML::LibXML::NodeList object matching all child elements
336         # of <config><opensrf>...
337         my $osrf_cfg = $config->findnodes('/config/opensrf/child::*');
338
339         # Iterate through the nodes to pull out key=>value pairs of config settings
340         foreach my $node ($osrf_cfg->get_nodelist()) {
341                 my $child_state = 0;
342
343                 # This will be overwritten if it's a scalar setting
344                 $bootstrap{$node->nodeName()} = [];
345
346                 foreach my $child_node ($node->childNodes) {
347                         # from libxml/tree.h: nodeType 1 = ELEMENT_NODE
348                         next if $child_node->nodeType() != 1;
349
350                         # If the child node is an element, this element may
351                         # have multiple values; therefore, push it into an array
352             my $content = OpenSRF::Utils::Config::extract_child($child_node);
353                         push(@{$bootstrap{$node->nodeName()}}, $content) if $content;
354                         $child_state = 1;
355                 }
356                 if (!$child_state) {
357                         $bootstrap{$node->nodeName()} = OpenSRF::Utils::Config::extract_text($node->textContent);
358                 }
359         }
360
361         my $section = $self->section_pkg->new(\%bootstrap);
362         my $sub_name = $section->SECTION;
363         $self->_sub_builder($sub_name);
364         $self->$sub_name($section);
365
366 }
367 sub extract_child {
368     my $node = shift;
369     use OpenSRF::Utils::SettingsParser;
370     return OpenSRF::Utils::SettingsParser::XML2perl($node);
371 }
372
373 sub extract_text {
374         my $self = shift;
375         $self =~ s/^\s*([.*?])\s*$//m;
376         return $self;
377 }
378
379 #------------------------------------------------------------------------------------------------------------------------------------
380
381 =head1 SEE ALSO
382
383         OpenSRF::Utils
384
385 =head1 LIMITATIONS
386
387 Elements containing heterogeneous child elements are treated as though they have the same element name;
388 for example:
389   <routers>
390     <router>localhost</router>
391     <furniture>chair</furniture>
392   </routers>
393
394 ... will simply generate a key=>value pair of C<< routers => ['localhost', 'chair'] >>.
395
396 =head1 BUGS
397
398 No known bugs, but report any to open-ils-dev@list.georgialibraries.org or mrylander@gmail.com.
399
400 =head1 COPYRIGHT AND LICENSING
401
402 Copyright (C) 2000-2007, Mike Rylander
403 Copyright (C) 2007, Laurentian University, Dan Scott <dscott@laurentian.ca>
404
405 The OpenSRF::Utils::Config module is free software. You may distribute under the terms
406 of the GNU General Public License version 2 or greater.
407
408 =cut
409
410
411 1;