a bit more work on WebPAC::Input::ISIS
[webpac2] / lib / WebPAC / Common.pm
1 package WebPAC::Common;
2
3 use warnings;
4 use strict;
5
6 use Log::Log4perl qw(get_logger :levels);
7
8 =head1 NAME
9
10 WebPAC::Common - internal methods called from other WebPAC modules
11
12 =head1 VERSION
13
14 Version 0.01
15
16 =cut
17
18 our $VERSION = '0.01';
19
20 =head1 SYNOPSYS
21
22 This module defines common functions, and is used as base for other, more
23 specific modules.
24
25  my $webpac = new WebPAC::Common(
26         filter => {
27                 'filter_name_1' => sub {
28                         # filter code
29                         return length($_);
30                 }, ...
31         },
32   }
33
34 =head1 FUNCTIONS
35
36 =head2 fill_in
37
38 Workhourse of all: takes record from in-memory structure of database and
39 strings with placeholders and returns string or array of with substituted
40 values from record.
41
42  my $text = $webpac->fill_in($rec,'v250^a');
43
44 Optional argument is ordinal number for repeatable fields. By default,
45 it's assume to be first repeatable field (fields are perl array, so first
46 element is 0).
47 Following example will read second value from repeatable field.
48
49  my $text = $webpac->fill_in($rec,'Title: v250^a',1);
50
51 This function B<does not> perform parsing of format to inteligenty skip
52 delimiters before fields which aren't used.
53
54 This method will automatically decode UTF-8 string to local code page
55 if needed.
56
57 =cut
58
59 sub fill_in {
60         my $self = shift;
61
62         my $log = $self->_get_logger();
63
64         my $rec = shift || $log->logconfess("need data record");
65         my $format = shift || $log->logconfess("need format to parse");
66         # iteration (for repeatable fields)
67         my $i = shift || 0;
68
69         $log->logdie("infitite loop in format $format") if ($i > ($self->{'max_mfn'} || 9999));
70
71         # FIXME remove for speedup?
72         $log->logconfess("need HASH as first argument!") if ($rec !~ /HASH/o);
73
74         if (utf8::is_utf8($format)) {
75                 $format = $self->_x($format);
76         }
77
78         my $found = 0;
79
80         my $eval_code;
81         # remove eval{...} from beginning
82         $eval_code = $1 if ($format =~ s/^eval{([^}]+)}//s);
83
84         my $filter_name;
85         # remove filter{...} from beginning
86         $filter_name = $1 if ($format =~ s/^filter{([^}]+)}//s);
87
88         # do actual replacement of placeholders
89         # repeatable fields
90         $format =~ s/v(\d+)(?:\^(\w))?/$self->get_data(\$rec,$1,$2,$i,\$found)/ges;
91         # non-repeatable fields
92         $format =~ s/s(\d+)(?:\^(\w))?/$self->get_data(\$rec,$1,$2,0,\$found)/ges;
93
94         if ($found) {
95                 $log->debug("format: $format");
96                 if ($eval_code) {
97                         my $eval = $self->fill_in($rec,$eval_code,$i);
98                         return if (! $self->_eval($eval));
99                 }
100                 if ($filter_name && $self->{'filter'}->{$filter_name}) {
101                         $log->debug("filter '$filter_name' for $format");
102                         $format = $self->{'filter'}->{$filter_name}->($format);
103                         return unless(defined($format));
104                         $log->debug("filter result: $format");
105                 }
106                 # do we have lookups?
107                 if ($self->{'lookup'}) {
108                         return $self->lookup($format);
109                 } else {
110                         return $format;
111                 }
112         } else {
113                 return;
114         }
115 }
116
117
118 =head2 get_data
119
120 Returns value from record.
121
122  my $text = $self->get_data(\$rec,$f,$sf,$i,\$found);
123
124 Arguments are:
125 record reference C<$rec>,
126 field C<$f>,
127 optional subfiled C<$sf>,
128 index for repeatable values C<$i>.
129
130 Optinal variable C<$found> will be incremeted if there
131 is field.
132
133 Returns value or empty string.
134
135 =cut
136
137 sub get_data {
138         my $self = shift;
139
140         my ($rec,$f,$sf,$i,$found) = @_;
141
142         if ($$rec->{$f}) {
143                 return '' if (! $$rec->{$f}->[$i]);
144                 no strict 'refs';
145                 if ($sf && $$rec->{$f}->[$i]->{$sf}) {
146                         $$found++ if (defined($$found));
147                         return $$rec->{$f}->[$i]->{$sf};
148                 } elsif ($$rec->{$f}->[$i]) {
149                         $$found++ if (defined($$found));
150                         # it still might have subfield, just
151                         # not specified, so we'll dump all
152                         if ($$rec->{$f}->[$i] =~ /HASH/o) {
153                                 my $out;
154                                 foreach my $k (keys %{$$rec->{$f}->[$i]}) {
155                                         $out .= $$rec->{$f}->[$i]->{$k}." ";
156                                 }
157                                 return $out;
158                         } else {
159                                 return $$rec->{$f}->[$i];
160                         }
161                 }
162         } else {
163                 return '';
164         }
165 }
166
167
168 =head2 progress_bar
169
170 Draw progress bar on STDERR.
171
172  $webpac->progress_bar($current, $max);
173
174 =cut
175
176 sub progress_bar {
177         my $self = shift;
178
179         my ($curr,$max) = @_;
180
181         my $log = $self->_get_logger();
182
183         $log->logconfess("no current value!") if (! $curr);
184         $log->logconfess("no maximum value!") if (! $max);
185
186         if ($curr > $max) {
187                 $max = $curr;
188                 $log->debug("overflow to $curr");
189         }
190
191         $self->{'last_pcnt'} ||= 1;
192         $self->{'start_t'} ||= time();
193
194         my $p = int($curr * 100 / $max) || 1;
195
196         # reset on re-run
197         if ($p < $self->{'last_pcnt'}) {
198                 $self->{'last_pcnt'} = $p;
199                 $self->{'start_t'} = time();
200         }
201
202         if ($p != $self->{'last_pcnt'}) {
203
204                 my $t = time();
205                 my $rate = ($curr / ($t - $self->{'start_t'} || 1));
206                 my $eta = ($max-$curr) / ($rate || 1);
207                 printf STDERR ("%5d [%-38s] %-5d %0.1f/s %s\r",$curr,"=" x ($p/3)."$p%>", $max, $rate, $self->fmt_time($eta));
208                 $self->{'last_pcnt'} = $p;
209                 $self->{'last_curr'} = $curr;
210         }
211         print STDERR "\n" if ($p == 100);
212 }
213
214 =head2 fmt_time
215
216 Format time (in seconds) for display.
217
218  print $webpac->fmt_time(time());
219
220 This method is called by L<progress_bar> to display remaining time.
221
222 =cut
223
224 sub fmt_time {
225         my $self = shift;
226
227         my $t = shift || 0;
228         my $out = "";
229
230         my ($ss,$mm,$hh) = gmtime($t);
231         $out .= "${hh}h" if ($hh);
232         $out .= sprintf("%02d:%02d", $mm,$ss);
233         $out .= "  " if ($hh == 0);
234         return $out;
235 }
236
237 #
238 #
239 #
240
241 =head1 INTERNAL METHODS
242
243 Here is a quick list of internal methods, mostly useful to turn debugging
244 on them (see L<LOGGING> below for explanation).
245
246 =cut
247
248 =head2 _eval
249
250 Internal function to eval code without C<strict 'subs'>.
251
252 =cut
253
254 sub _eval {
255         my $self = shift;
256
257         my $code = shift || return;
258
259         my $log = $self->_get_logger();
260
261         no strict 'subs';
262         my $ret = eval $code;
263         if ($@) {
264                 $log->error("problem with eval code [$code]: $@");
265         }
266
267         $log->debug("eval: ",$code," [",$ret,"]");
268
269         return $ret || undef;
270 }
271
272 =head2 _sort_by_order
273
274 Sort xml tags data structure accoding to C<order=""> attribute.
275
276 =cut
277
278 sub _sort_by_order {
279         my $self = shift;
280
281         my $va = $self->{'import_xml'}->{'indexer'}->{$a}->{'order'} ||
282                 $self->{'import_xml'}->{'indexer'}->{$a};
283         my $vb = $self->{'import_xml'}->{'indexer'}->{$b}->{'order'} ||
284                 $self->{'import_xml'}->{'indexer'}->{$b};
285
286         return $va <=> $vb;
287 }
288
289 =head2 _x
290
291 Convert string from UTF-8 to code page defined in C<import_xml>.
292
293  my $text = $webpac->_x('utf8 text');
294
295 Default application code page is C<ISO-8859-2>. You will probably want to
296 change that when creating new instance of object based on this one.
297
298 =cut
299
300 sub _x {
301         my $self = shift;
302         my $utf8 = shift || return;
303
304         # create UTF-8 convertor for import_xml files
305         $self->{'utf2cp'} ||= Text::Iconv->new('UTF-8' ,$self->{'code_page'} || 'ISO-8859-2');
306
307         return $self->{'utf2cp'}->convert($utf8) ||
308                 $self->_get_logger()->logwarn("can't convert '$utf8'");
309 }
310
311 =head2 _init_logger
312
313 This function will init C<Log::Log4perl> using provided configuration file.
314
315   $webpac->_init_logger('/path/to/log.conf');
316
317 If no path to configuration file is given, dummy empty configuration
318 will be create.
319
320 =cut
321
322 sub _init_logger {
323         my $self = shift;
324         my $file = shift;
325         if ($file) {
326                 Log::Log4perl->init($file);
327         } else {
328                 my $conf = q( );
329                 Log::Log4perl->init( \$conf );
330         }
331 }
332
333
334 =head2 _get_logger
335
336 Get C<Log::Log4perl> object with a twist: domains are defined for each
337 method
338
339  my $log = $webpac->_get_logger();
340
341 =cut
342
343 sub _get_logger {
344         my $self = shift;
345
346         $self->{'_logger_ok'} ||= $self->_init_logger;
347
348         my $name = (caller(1))[3] || caller;
349         return get_logger($name);
350 }
351
352
353 =head1 LOGGING
354
355 Logging in WebPAC is performed by L<Log::Log4perl> with config file
356 C<log.conf>.
357
358 Methods defined above have different levels of logging, so
359 it's descriptions will be useful to turn (mostry B<debug> logging) on
360 or off to see why WabPAC isn't perforing as you expect it (it might even
361 be a bug!).
362
363 B<This is different from normal Log4perl behaviour>. To repeat, you can
364 also use method names, and not only classes (which are just few)
365 to filter logging.
366
367