File Coverage

blib/lib/App/CELL/Load.pm
Criterion Covered Total %
statement 264 300 88.0
branch 83 112 74.1
condition 16 29 55.1
subroutine 27 28 96.4
pod 7 7 100.0
total 397 476 83.4


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2020, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             package App::CELL::Load;
34              
35 14     14   25865 use strict;
  14         39  
  14         381  
36 14     14   66 use warnings;
  14         19  
  14         333  
37 14     14   200 use 5.012;
  14         40  
38              
39 14     14   1061 use App::CELL::Config qw( $meta $core $site );
  14         26  
  14         1207  
40 14     14   84 use App::CELL::Log qw( $log );
  14         22  
  14         1423  
41 14     14   82 use App::CELL::Message;
  14         36  
  14         364  
42 14     14   77 use App::CELL::Status;
  14         37  
  14         397  
43 14     14   5292 use App::CELL::Test qw( cmp_arrays );
  14         32  
  14         771  
44 14     14   88 use App::CELL::Util qw( stringify_args is_directory_viable );
  14         35  
  14         537  
45 14     14   69 use Data::Dumper;
  14         37  
  14         490  
46 14     14   5459 use File::Next;
  14         23925  
  14         408  
47 14     14   6121 use File::ShareDir;
  14         254408  
  14         676  
48 14     14   98 use Params::Validate qw( :all );
  14         25  
  14         16607  
49              
50             =head1 NAME
51              
52             App::CELL::Load -- find and load message files and config files
53              
54              
55              
56             =head1 SYNOPSIS
57            
58             use App::CELL::Load;
59              
60             # Load App::CELL's internal messages and config params and then
61             # attempt to load the application's messages and config params
62             $status = App::CELL::Load::init();
63             return $status if $status->not_ok;
64              
65             # attempt to determine the site configuration directory
66             my $resulthash = App::CELL::Load::get_sitedir();
67              
68             # get a reference to a list of configuration files (full paths) of a
69             # given type under a given directory
70             my $metafiles = App::CELL::Load::find_files( '/etc/CELL', 'meta' );
71            
72             # load messages from all message file in a given directory and all its
73             # subdirectories
74             $status = message_files( '/etc/CELL' );
75              
76             # load meta, core, and site params from all meta, core, and site
77             # configuration files in a given directory and all its subdirectories
78             $status = meta_core_site_files( '/etc/CELL' );
79              
80              
81              
82             =head1 DESCRIPTION
83              
84             The purpose of the App::CELL::Load module is to provide message and config
85             file finding and loading functionality to the App::CELL::Message and
86             App::CELL::Config modules.
87              
88              
89              
90             =head1 PACKAGE VARIABLES
91              
92             This module provides the following package variables
93              
94             =over
95              
96             =item C<$sharedir> - the full path of the sharedir
97              
98             =item C<$sharedir_loaded> - whether it has been loaded or not
99              
100             =item C<@sitedir> - the full path of the site configuration directory
101              
102             =back
103              
104             =cut
105              
106             our $sharedir = '';
107             our $sharedir_loaded = 0;
108             our @sitedir = ();
109              
110              
111             =head1 MODULES
112              
113             =head2 init
114              
115             Idempotent initialization function.
116              
117             Optionally takes a PARAMHASH. The following arguments are recognized:
118              
119             =over
120              
121             =item C -- full path to the/a site dir
122              
123             =item C -- name of environment variable containing sitedir path
124              
125             =item C -- increase logging verbosity of the load routine
126              
127             =back
128              
129             E.g.:
130              
131             my $status = App::CELL::Load::init(
132             sitedir => '/etc/foo',
133             verbose => 1
134             );
135              
136             See L for details.
137              
138             =cut
139              
140             sub init {
141 13     13 1 1163 my %ARGS = validate( @_, {
142             enviro => { type => SCALAR, optional => 1 },
143             sitedir => { type => SCALAR, optional => 1 },
144             verbose => { type => SCALAR, default => 0 },
145             } );
146              
147             # determine verbosity level
148 13         69 my $args_string;
149 13 50       39 if ( %ARGS ) {
150 13         53 $args_string = "with arguments: " . stringify_args( \%ARGS );
151             } else {
152 0         0 $args_string = "without arguments";
153             }
154 13   100     457 $meta->set('CELL_META_LOAD_VERBOSE', $ARGS{'verbose'} || 0);
155              
156 13 100       109 $log->info(
157             "Entering App::CELL::Load::init from " . (caller)[0] . " $args_string",
158             cell => 1
159             ) if $meta->CELL_META_LOAD_VERBOSE;
160              
161             # check for taint mode
162 13 50       123 if ( ${^TAINT} != 0 ) {
163 0         0 return App::CELL::Status->new( level => "FATAL",
164             code => "Attempt to load while in taint mode (-T)" );
165             }
166              
167             # look up sharedir
168 13 100       43 if ( not $sharedir ) {
169 9         57 my $tmp_sharedir = File::ShareDir::dist_dir('App-CELL');
170 9 50       1018 if ( ! is_directory_viable( $tmp_sharedir ) ) {
171 0         0 return App::CELL::Status->new(
172             level => 'ERR',
173             code => 'CELL_SHAREDIR_NOT_VIABLE',
174             args => [ $tmp_sharedir, $App::CELL::Util::not_viable_reason ],
175             );
176             }
177 9 100       75 $log->info( "Found viable CELL configuration directory " .
178             $tmp_sharedir . " in App::CELL distro", cell => 1 ) if $meta->CELL_META_LOAD_VERBOSE;
179 9         48 $site->set( 'CELL_SHAREDIR_FULLPATH', $tmp_sharedir );
180 9         29 $sharedir = $tmp_sharedir;
181             }
182              
183             # walk sharedir
184 13 100 66     76 if ( $sharedir and not $sharedir_loaded ) {
185 9         56 my $status = message_files( $sharedir );
186 9         79 my $load_status = _report_load_status( $sharedir, 'sharedir', 'message', $status );
187 9 50       39 return $load_status if $load_status->not_ok;
188 9         33 $status = meta_core_site_files( $sharedir );
189 9         44 $load_status = _report_load_status( $sharedir, 'sharedir', 'config params', $status );
190 9 50       50 return $load_status if $load_status->not_ok;
191 9         38 $site->set( 'CELL_SHAREDIR_LOADED', 1 );
192 9         34 $sharedir_loaded = 1;
193             }
194              
195 13 100       63 if ( $meta->CELL_META_LOAD_VERBOSE ) {
196 6 50       23 if ( @sitedir ) {
197 0         0 $log->debug( "sitedir package variable contains ->" .
198             join( ':', @sitedir ) . "<-", cell => 1 );
199             } else {
200 6         32 $log->debug( "sitedir package variable is empty", cell => 1 );
201             }
202             }
203              
204             # get sitedir from args or environment
205 13         56 my $status = get_sitedir( %ARGS );
206 13 100       49 return $status unless $status->ok;
207 2         9 my $sitedir_candidate = $status->payload;
208              
209             # walk sitedir
210 2 50       6 if ( $sitedir_candidate ) {
211 2         7 my $status = message_files( $sitedir_candidate );
212 2         9 my $messages_loaded = _report_load_status( $sitedir_candidate, 'sitedir', 'message', $status );
213 2         7 $status = meta_core_site_files( $sitedir_candidate );
214 2         7 my $params_loaded = _report_load_status( $sitedir_candidate, 'sitedir', 'config params', $status );
215             #
216             # sitedir candidate is accepted only if something is actually
217             # loaded
218             #
219 2 50 33     9 if ( $messages_loaded->ok or $params_loaded->ok ) {
220 2         13 $meta->set( 'CELL_META_SITEDIR_LOADED',
221             ( $meta->CELL_META_SITEDIR_LOADED + 1 ) );
222 2         6 push @sitedir, $sitedir_candidate;
223 2         8 $meta->set( 'CELL_META_SITEDIR_LIST', \@sitedir );
224             }
225             }
226              
227             # check that at least sharedir has really been loaded
228             SANITY: {
229 2         8 my $results = [];
  2         3  
230              
231             # remember, message constructor returns a status object
232 2         7 my $status = App::CELL::Message->new( code => 'CELL_LOAD_SANITY_MESSAGE' );
233              
234 2 50       7 if ( $status->ok ) {
235 2         6 my $msgobj = $status->payload;
236 2         15 push @$results, (
237             $meta->CELL_LOAD_SANITY_META,
238             $core->CELL_LOAD_SANITY_CORE,
239             $site->CELL_LOAD_SANITY_SITE,
240             $msgobj->text(),
241             );
242 2         15 my $cmp_arrays_result = cmp_arrays(
243             $results,
244             [ 'Baz', 'Bar', 'Foo', 'This is a sanity testing message' ],
245             );
246 2 50       14 last SANITY if $cmp_arrays_result;
247             }
248 0         0 return App::CELL::Status->new(
249             level => 'ERR',
250             code => 'CELL_LOAD_FAILED_SANITY',
251             );
252             }
253            
254 2 50       12 $log->debug( "Leaving App::CELL::Load::init", cell => 1 )
255             if $meta->CELL_META_LOAD_VERBOSE;
256              
257 2         6 return App::CELL::Status->ok;
258             }
259              
260              
261             sub _report_load_status {
262 22     22   72 my ( $dir_path, $dir_desc, $what, $status ) = @_;
263 22         78 my $return_status = App::CELL::Status->ok;
264 22   50     37 my $quantitems = ${ $status->payload }{quantitems} || 0;
265 22   50     36 my $quantfiles = ${ $status->payload }{quantfiles} || 0;
266 22 50       89 if ( $quantitems == 0 ) {
267 0         0 $return_status = App::CELL::Status->new(
268             level => 'WARN',
269             code => 'CELL_DIR_WALKED_NOTHING_FOUND',
270             args => [ $what, $dir_desc, $dir_path, $quantfiles ],
271             caller => [ caller ],
272             cell => 1,
273             );
274             }
275             # trigger a log message: note that we can't use an OK status here
276             # because log messages for those are suppressed
277             App::CELL::Status->new (
278 22 100 66     164 level => 'INFO',
      100        
279             code => 'CELL_DIR_WALKED_ITEMS_LOADED',
280             args => [ $quantitems, $what, $quantfiles, $dir_desc, $dir_path ],
281             caller => [ caller ],
282             cell => 1,
283             ) if ( $dir_desc eq 'sitedir' ) or ( $dir_desc eq 'sharedir' and $meta->CELL_META_LOAD_VERBOSE );
284 22         86 return $return_status;
285             }
286              
287             =head2 message_files
288              
289             Loads message files from the given directory. Takes: full path to
290             configuration directory. Returns: result hash containing 'quantfiles'
291             (total number of files processed) and 'count' (total number of
292             messages loaded).
293              
294             =cut
295              
296             sub message_files {
297              
298 11     11 1 27 my $confdir = shift;
299 11         23 my %reshash;
300 11         164 $reshash{quantfiles} = 0;
301 11         50 $reshash{quantitems} = 0;
302              
303 11         39 my $file_list = find_files( 'message', $confdir );
304              
305 11 50       52 if ( @$file_list ) {
306 11 100       58 $log->info( "Found message files: " . join( ',', @$file_list ),
307             cell => 1 ) if $meta->CELL_META_LOAD_VERBOSE;
308             } else {
309 0 0       0 $log->warn( "No message files found in $confdir", cell => 1 )
310             if $meta->CELL_META_LOAD_VERBOSE;
311             }
312              
313 11         70 foreach my $file ( @$file_list ) {
314 12         31 $reshash{quantfiles} += 1;
315 12 50       61 die "INTERNAL ERROR (App::CELL::Message::mesg is not a reference)" if not ref( $App::CELL::Message::mesg );
316 12         47 $reshash{quantitems} += parse_message_file(
317             File => $file,
318             Dest => $App::CELL::Message::mesg,
319             );
320             }
321              
322 11         87 return App::CELL::Status->new(
323             level => 'OK',
324             payload => \%reshash,
325             );
326             }
327              
328              
329             =head2 meta_core_site_files
330              
331             Loads meta, core, and site config files from the given directory. Takes:
332             full path to configuration directory. Returns: result hash containing
333             'quantfiles' (total number of files processed) and 'count' (total number of
334             configuration parameters loaded).
335              
336             =cut
337              
338             sub meta_core_site_files {
339              
340 11     11 1 31 my $confdir = shift;
341 11         26 my %reshash;
342 11         26 $reshash{quantfiles} = 0;
343 11         21 $reshash{quantitems} = 0;
344              
345 11         30 foreach my $type ( 'meta', 'core', 'site' ) {
346 33         78 my $fulltype = 'App::CELL::Config::' . $type;
347             #$log->debug( "\$fulltype is $fulltype", cell => 1 );
348 33         75 my $file_list = find_files( $type, $confdir );
349 33         71 foreach my $file ( @$file_list ) {
350 14     14   114 no strict 'refs';
  14         48  
  14         9792  
351 29         54 $reshash{quantfiles} += 1;
352 29         119 $reshash{quantitems} += parse_config_file(
353             File => $file,
354             Dest => $$fulltype,
355             );
356             }
357             }
358              
359 11         77 return App::CELL::Status->new(
360             level => 'OK',
361             payload => \%reshash,
362             );
363             }
364              
365              
366             =head2 get_sitedir
367              
368             This function implements the algorithm described in
369             L to find a sitedir candidate.
370             configuration directory.
371              
372             On success -- i.e., as soon as the algorithm finds a viable sitedir
373             candidate -- the sitedir (full path) is added to CELL_META_SITEDIR_LIST and
374             an OK status object is returned, with the sitedir in the payload.
375              
376             On failure, the function returns an ERR or WARN status object containing
377             a description of what went wrong.
378              
379             =cut
380              
381             sub get_sitedir {
382              
383 13     13 1 38 my %paramhash = @_;
384 13         27 my $reason;
385              
386 13         26 my ( $sitedir, $log_message, $status );
387             GET_CANDIDATE_DIR: {
388              
389             # look in paramhash for sitedir
390 13         20 $log->debug( "SITEDIR SEARCH, ROUND 1 (sitedir parameter):", cell => 1 );
  13         63  
391 13 100       48 if ( $sitedir = $paramhash{sitedir} ) {
392 3         8 $log_message = "Viable sitedir passed as argument";
393 3 100       10 last GET_CANDIDATE_DIR if is_directory_viable( $sitedir );
394 1         7 $reason = "CELL load routine received 'sitedir' argument ->$sitedir<- " .
395             "but this is not a viable directory ($App::CELL::Util::not_viable_reason)";
396 1         8 $log->err( $reason, cell => 1 );
397 1         6 return App::CELL::Status->new( level => 'ERR', code => $reason );
398             }
399 10         64 $log->debug( "looked at function arguments but they do not " .
400             "contain a literal site dir path", cell => 1 );
401              
402             # look in paramhash for name of environment variable
403 10         54 $log->debug( "SITEDIR SEARCH, ROUND 2 (enviro parameter):", cell => 1 );
404 10 100       43 if ( $paramhash{enviro} )
405             {
406 1 50       8 if ( $sitedir = $ENV{ $paramhash{enviro} } ) {
407             $log_message = "Found viable sitedir in " . $paramhash{enviro}
408 0         0 . " environment variable";
409 0 0       0 last GET_CANDIDATE_DIR if is_directory_viable( $sitedir );
410 0         0 $reason = "CELL load routine received 'enviro' argument ->$paramhash{enviro}<- " .
411             "which expanded to ->$sitedir<- but this is not a viable directory " .
412             "($App::CELL::Util::not_viable_reason)";
413 0         0 return App::CELL::Status->new( level => 'ERR', code => $reason );
414             } else {
415 1         7 $reason = "CELL load routine: enviro argument contained ->$paramhash{enviro}<- " .
416             "but no such variable found in the environment";
417 1         5 return App::CELL::Status->new( level => 'ERR', code => $reason );
418             }
419             }
420              
421             # fall back to hard-coded environment variable
422 9         50 $log->debug( "SITEDIR SEARCH, ROUND 3 (fallback to CELL_SITEDIR " .
423             "environment variable):", cell => 1 );
424 9         24 $sitedir = undef;
425 9 100       42 if ( $sitedir = $ENV{ 'CELL_SITEDIR' } ) {
426 1         3 $log_message = "Found viable sitedir in CELL_SITEDIR environment variable";
427 1 50       7 last GET_CANDIDATE_DIR if is_directory_viable( $sitedir );
428 1         8 $reason = "CELL load routine: no 'sitedir', 'enviro' arguments specified; " .
429             "fell back to CELL_SITEDIR environment variable, which exists " .
430             "with value ->$sitedir<- but this is not a viable directory" .
431             "($App::CELL::Util::not_viable_reason)";
432 1 50       7 if ( $meta->CELL_META_SITEDIR_LOADED ) {
433 0         0 $log->warn( $reason, cell => 1 );
434             $log->notice( "The following sitedirs have been loaded already " .
435 0         0 join( ' ', @{ $meta->CELL_META_SITEDIR_LIST }),
  0         0  
436             cell => 1 );
437 0         0 return App::CELL::Status->ok;
438             }
439 1         6 return App::CELL::Status->new( level => 'WARN', code => $reason );
440             }
441            
442             # failed to find a sitedir
443 8         18 $reason = "CELL load routine gave up (no sitedir argument, no enviro " .
444             "argument, no CELL_SITEDIR environment variable)";
445 8 50       51 if ( $meta->CELL_META_SITEDIR_LOADED ) {
446 0         0 $log->warn( $reason, cell => 1 );
447             $log->notice( "The following sitedirs have been loaded already " .
448 0         0 join( ' ', @{ $meta->CELL_META_SITEDIR_LIST } ),
  0         0  
449             cell => 1 );
450 0         0 return App::CELL::Status->ok;
451             }
452 8         42 return App::CELL::Status->new( level => 'WARN', code => $reason );
453             }
454              
455             # SUCCEED
456 2         16 $log->info( $log_message, cell => 1 );
457 2         7 return App::CELL::Status->ok( $sitedir );
458             }
459              
460              
461             =head2 find_files
462              
463             Takes two arguments: full directory path and config file type.
464              
465             Always returns an array reference. On "failure", the array reference will
466             be empty.
467              
468             How it works: first, the function checks a state variable to see if the
469             "work" of walking the configuration directory has already been done. If
470             so, then the function simply returns the corresponding array reference from
471             its cache (the state hash C<%resultlist>). If this is the first invocation
472             for this directory, the function walks the directory (and all its
473             subdirectories) to find files matching one of the four regular expressions
474             corresponding to the four types of configuration files('meta', 'core',
475             'site', 'message'). For each matching file, the full path is pushed onto
476             the corresponding array in the cache.
477              
478             Note that there is a ceiling on the number of files that will be considered
479             while walking the directory tree. This ceiling is defined in the package
480             variable C<$max_files> (see below).
481              
482             =cut
483              
484             # regular expressions for each file type
485             our $typeregex = {
486             'meta' => qr/^.+_MetaConfig.pm$/ ,
487             'core' => qr/^.+_Config.pm$/ ,
488             'site' => qr/^.+_SiteConfig.pm$/ ,
489             'message' => qr/^.+_Message(_[^_]+){0,1}.conf$/ ,
490             };
491              
492             # C<$max_files> puts a limit on how many files we will look at in our directory
493             # tree walk
494             our $max_files = 1000;
495              
496             sub find_files {
497 50     50 1 589 my ( $type, $dirpath ) = @_;
498              
499             #
500             # FIXME: convert $dirpath into an absolute path so it's always the same
501             #
502              
503             # re-entrant function
504 14     14   122 use feature "state";
  14         29  
  14         18251  
505 50         80 state $resultcache = {};
506              
507             # If $dirpath key exists in %resultcache, we are re-entering.
508             # In other words, $dirpath has already been walked and all the
509             # filepaths are already in the array stored within %resultcache
510 50 100       160 if ( exists $resultcache->{ $dirpath } ) {
511 38 100       166 $log->debug( "Re-entering find_files for $dirpath (type '$type')",
512             cell => 1) if $meta->CELL_META_LOAD_VERBOSE;
513 38         99 return $resultcache->{ $dirpath }->{ $type };
514             } else { # create it
515 12         82 $resultcache->{ $dirpath } = {
516             'meta' => [],
517             'core' => [],
518             'site' => [],
519             'message' => [],
520             };
521             }
522              
523             # walk the directory (do we need some error checking here?)
524 12         169 $log->debug( "Preparing to walk $dirpath", cell => 1 );
525 12         61 my $iter = File::Next::files( $dirpath );
526              
527             # while we are walking, go ahead and populate the result cache for _all
528             # four_ types (even though we were asked for just one type)
529 12         1334 my $walk_counter = 0;
530 12         37 ITER_LOOP: while ( defined ( my $file = $iter->() ) ) {
531 57         4098 $log->debug( "Now considering $file", cell => 1 );
532 57         86 $walk_counter += 1;
533 57 50       121 if ( $walk_counter > $max_files ) {
534 0         0 App::CELL::Status->new (
535             level => 'ERROR',
536             code => 'Maximum number of configuration file candidates ->%s<- exceeded in %s',
537             args => [ $max_files, $dirpath ],
538             );
539 0         0 last ITER_LOOP; # stop looping if there are so many files
540             }
541 57 50       979 if ( not -r $file ) {
542 0         0 App::CELL::Status->new (
543             level => 'WARN',
544             code => 'Load operation passed over file ->%s<- (not readable)',
545             args => [ $file ],
546             );
547 0         0 next ITER_LOOP; # jump to next file
548             }
549             # $file is now a "candidate"
550 57         191 my $counter = 0;
551 57         118 foreach my $type ( 'meta', 'core', 'site', 'message' ) {
552 163 100       223 if ( $file =~ /${ $typeregex }{ $type }/ ) {
  163         886  
553 47         78 push @{ $resultcache->{ $dirpath}->{ $type } }, $file;
  47         186  
554 47         73 $counter += 1;
555 47         152 next ITER_LOOP;
556             }
557             }
558 10 100 66     142 $log->info( "Load operation passed over file $file (type not " .
559             "recognized)", cell => 1 ) if not $counter and $meta->CELL_META_LOAD_VERBOSE;
560             }
561 12 100       180 $log->debug( "Returning " . join( ',', @{ $resultcache->{ $dirpath }->{ $type } } ),
  6         48  
562             cell => 1 ) if $meta->CELL_META_LOAD_VERBOSE;
563 12         174 return $resultcache->{ $dirpath }->{ $type };
564             }
565              
566              
567             =head2 parse_message_file
568              
569             This function is where message files are parsed. It takes a PARAMHASH
570             consisting of:
571              
572             =over
573              
574             =item C - filename (full path)
575              
576             =item C - hash reference (where to store the message templates).
577              
578             =back
579              
580             Returns: number of stanzas successfully parsed and loaded
581              
582             =cut
583              
584             sub parse_message_file {
585 13     13 1 89 my @ARGS = @_;
586 13         154 my %ARGS = (
587             'File' => undef,
588             'Dest' => undef,
589             @ARGS,
590             );
591              
592             my $process_stanza_sub = sub {
593              
594             # get arguments
595 184     184   334 my ( $file, $line, $lang, $stanza, $destref ) = @_;
596              
597             # put first token on first line into $code
598 184         572 my ( $code ) = $stanza->[0] =~ m/^\s*(\S+)/;
599 184 50       322 if ( not $code ) {
600 0         0 $log->info(
601             "ERROR: Could not process stanza ->" . join( " ", @$stanza ) . "<- in $file",
602             cell => 1,
603             );
604 0         0 return 0;
605             }
606              
607             # The rest of the lines are the message template
608 184         236 my $text = '';
609 184         247 foreach ( @$stanza[1 .. $#{ $stanza }] ) {
  184         395  
610 193         239 chomp;
611 193         464 $text = $text . " " . $_;
612             }
613 184         585 $text =~ s/^\s+//g;
614 184 50 33     693 if ( $code and $lang and $text ) {
      33        
615 184         991 $log->debug(
616             "Parsed message CODE ->$code<- LANG ->$lang<- TEXT ->$text<-",
617             cell => 1,
618             );
619             # we have a candidate, but we don't want to overwrite
620             # an existing entry with the same $code-$lang pair
621 184 100       543 if ( $destref->{ $code }->{ $lang } ) {
622 1         2 my $existing_text = $destref->{ $code }->{ $lang }->{ 'Text' };
623             $log->error(
624             "ERROR: not loading code-lang pair ->$code" .
625             "/$lang<- with text ->$text<- because this would" .
626 1         10 " overwrite existing pair from " . $destref->{$code}->{$lang}->{'File'},
627             cell => 1,
628             );
629 1         3 return 0;
630             } else {
631 183 100       766 $log->debug(
632             "OK: loading code-lang pair ->$code/$lang<- with text ->$text<-",
633             cell => 1,
634             ) if $meta->CELL_META_LOAD_VERBOSE;
635 183         696 $destref->{ $code }->{ $lang } = {
636             'Text' => $text,
637             'File' => $file,
638             'Line' => $line,
639             };
640 183         327 return 1;
641             }
642             }
643             $log->error(
644 0   0     0 "Parsed " . ( $code || "" ) . " but something missing!!",
645             cell => 1,
646             );
647 0         0 return 0;
648 13         94 };
649              
650             # determine language from file name
651 13         130 my ( $lang ) = $ARGS{'File'} =~ m/_Message_([^_]+).conf$/;
652 13 100       68 if ( not $lang ) {
653 1         14 $log->warn(
654             "Could not determine language from filename $ARGS{'File'} " .
655             "-- reverting to default language ->en<-",
656             cell => 1,
657             );
658 1         2 $lang = 'en';
659             }
660              
661             # open the file for reading
662 13 50       472 open( my $fh, "<", $ARGS{'File'} )
663             or die "cannot open < $ARGS{'File'}: $!";
664              
665 13         69 my @stanza = ();
666 13         98 my $index = 0;
667 13         47 my $count = 0;
668 13         21 my $line = 0;
669 13         315 while ( <$fh> ) {
670 1036         1324 chomp( $_ );
671 1036         1090 $line += 1;
672             #$log->debug( "Read line =>$_<= from $ARGS{'File'}", cell => 1 );
673 1036 100       1964 $_ = '' if /^\s+$/;
674 1036 100       1371 if ( $_ ) {
675 840 100       2015 if ( ! /^\s*#/ ) {
676 377         1159 s/^\s*//g;
677 377         2147 s/\s*$//g;
678 377         1202 $stanza[ $index++ ] = $_;
679             }
680             } else {
681             $count += &$process_stanza_sub( $ARGS{'File'}, $line, $lang, \@stanza,
682 196 100       493 $ARGS{'Dest'} ) if @stanza;
683 196         309 @stanza = ();
684 196         561 $index = 0;
685             }
686             }
687             # There might be one stanza left at the end
688             $count += &$process_stanza_sub( $ARGS{'File'}, $line, $lang, \@stanza,
689 13 100       105 $ARGS{'Dest'} ) if @stanza;
690              
691 13         132 close $fh;
692              
693             # $log->info( "Parsed and loaded $count configuration stanzas "
694             # . "from $ARGS{'File'}", cell => 1 );
695            
696 13         227 return $count;
697             };
698              
699              
700             =head2 parse_config_file
701              
702             Parses a configuration file and adds the parameters found to the hashref
703             provided. If a parameter already exists in the hashref, a warning is
704             generated, the existing parameter is not overwritten, and processing
705             continues.
706              
707             This function doesn't care what type of configuration parameters
708             are in the file, except that they must be scalar values. Since the
709             configuration files are actually Perl modules, the value can even be
710             a reference (to an array, a hash, or a subroutine, or any other complex
711             data structure).
712              
713             The technique used in the C, derived from Request Tracker, can be
714             described as follows: a local typeglob "set" is defined, containing a
715             reference to an anonymous subroutine. Subsequently, a config file (Perl
716             module) consisting of calls to this "set" subroutine is Cd.
717              
718             Note: If even one call to C fails to compile, the entire file will be
719             rejected and no configuration parameters from that file will be loaded.
720              
721             The C function takes a PARAMHASH consisting of:
722              
723             =over
724              
725             =item C - filename (full path)
726              
727             =item C - hash reference (where to store the config params).
728              
729             =back
730              
731             Returns: number of configuration parameters parsed/loaded
732              
733             (IMPORTANT NOTE: If even one call to C fails to compile, the entire
734             file will be rejected and no configuration parameters from that file will
735             be loaded.)
736              
737             =cut
738              
739             sub parse_config_file {
740 31     31 1 416 my %ARGS = (
741             'File' => undef,
742             'Dest' => undef,
743             @_,
744             );
745              
746             # This is so we can use the C<$self> variable (in the C
747             # statement, below) to reach the C<_conf_from_config> functions from
748             # the configuration file.
749 31         62 my $self = {};
750 31         89 bless $self, 'App::CELL::Load';
751              
752 31         51 my $count = 0;
753            
754             # ideally this should be 'debug' for sharedir and 'info' for sitedir
755             # but in this routine I have no easy way of telling one from the other
756 31         174 $log->debug( "Loading =>$ARGS{'File'}<=", cell => 1 );
757 31 50       110 if ( not ref( $ARGS{'Dest'} ) ) {
758 0         0 $log->warn(
759             "Something strange happened: destination is not a reference?!?",
760             cell => 1,
761             );
762             }
763              
764             {
765 14     14   115 use Try::Tiny;
  14         42  
  14         8306  
  31         50  
766             try {
767             local *set = sub(@) {
768 156         234 my $number_of_params = scalar @_;
769 156         277 my @params = @_;
770 156         190 my $param;
771             my $value;
772 156 50       394 if ( $number_of_params == 0 ) {
    100          
    100          
773 0         0 my $msg = "set() called with no parameters";
774 0         0 $log->crit( $msg, cell => 1 );
775 0         0 die $msg;
776             } elsif ( $number_of_params == 1 ) {
777 1         1 $param = $params[0];
778 1         9 $log->warn(
779             "set() called with parameter $param but no value - set to \"\"",
780             cell => 1,
781             );
782             } elsif ( $number_of_params == 2 ) {
783 154         189 $param = $params[0];
784 154         216 $value = $params[1];
785 154         666 $log->debug(
786             "set() called with parameter $param and one value",
787             cell => 1,
788             );
789             } else {
790 1         2 $param = $params[0];
791 1         1 $value = $params[1];
792 1         6 $log->warn(
793             "set() called with $number_of_params parameters. Only " .
794             "the first two were used; the rest were ignored.",
795             cell => 1,
796             );
797             }
798 156         364 my ( undef, $file, $line ) = caller;
799             $count += $self->_conf_from_config(
800 156         412 'Dest' => $ARGS{'Dest'},
801             'Param' => $param,
802             'Value' => $value,
803             'File' => $file,
804             'Line' => $line,
805             );
806 31     31   1345 };
807 31         13225 require $ARGS{'File'};
808             }
809             catch {
810 0     0   0 my $errmsg = $_;
811 0         0 $errmsg =~ s/\012/ -- /g;
812 0         0 $log->err(
813             "CELL_CONFIG_LOAD_FAIL on file $ARGS{File} with error message: $errmsg",
814             cell => 1,
815             );
816 0         0 $log->debug( "The count is $count", cell => 1 );
817 0         0 return $count;
818 31         273 };
819             }
820             #$log->info( "Successfully loaded $count configuration parameters "
821             # . "from $ARGS{'File'}", cell => 1 );
822              
823 31         643 return $count;
824             }
825              
826              
827             =head2 _conf_from_config
828              
829             This function takes a target hashref (which points to one of the 'meta',
830             'core', or 'site' package hashes in C), a config parameter
831             (i.e. a string), config value, config file name, and line number.
832              
833             Let's imagine that the configuration parameter is "FOO_BAR". The function
834             first checks if a key named "FOO_BAR" already exists in the package hash
835             (which is passed into the function as C<%ARGS{'Dest'}>). If there isn't
836             one, it creates that key. If there is one, it leaves it untouched and
837             triggers a warning.
838              
839             Although the arguments are passed to the function in the form of a
840             PARAMHASH, the function converts them into ordinary private variables.
841             This was necessary to avoid extreme notational ugliness.
842              
843             =cut
844              
845             sub _conf_from_config {
846 156     156   206 my $self = shift;
847 156         3603 my ( %ARGS ) = validate( @_, {
848             Dest => { type => HASHREF },
849             Param => { type => SCALAR },
850             Value => { type => SCALAR|SCALARREF|ARRAYREF|HASHREF|CODEREF|UNDEF },
851             File => { type => SCALAR },
852             Line => { type => SCALAR },
853             } );
854             # convert PARAMHASH into private variables
855 156         820 my $desthash = $ARGS{'Dest'};
856 156         200 my $param = $ARGS{'Param'};
857 156         232 my $value = $ARGS{'Value'};
858 156         190 my $file = $ARGS{'File'};
859 156         185 my $line = $ARGS{'Line'};
860              
861 156 100       173 if ( keys( %{ $desthash->{ $param } } ) )
  156         502  
862             {
863             $log->warn(
864             "ignoring duplicate definition of config parameter $param in line $line " .
865             "of config file $file because it conflicts with a similar parameter in " .
866 2         29 $desthash->{ $param }->{'File'},
867             cell => 1,
868             );
869 2         9 return 0;
870             } else {
871 154         373 $desthash->{ $param } = {
872             'Value' => $value,
873             'File' => $file,
874             'Line' => $line,
875             };
876 154 100       660 $log->debug(
877             "Parsed parameter $param from $file, line $line",
878             cell => 1,
879             suppress_caller => 1
880             ) if $meta->CELL_META_LOAD_VERBOSE;
881 154         558 return 1;
882             }
883             }
884              
885             1;