File Coverage

blib/lib/Logwatch/RecordTree.pm
Criterion Covered Total %
statement 236 247 95.5
branch 69 90 76.6
condition 22 30 73.3
subroutine 31 32 96.8
pod 12 16 75.0
total 370 415 89.1


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #===============================================================================
3             # PODNAME: Logwatch::RecordTree
4             # ABSTRACT: an object to collect and print Logwatch events
5             #
6             # AUTHOR: Reid Augustin (REID)
7             # EMAIL: reid@hellosix.com
8             # CREATED: Thu Mar 12 18:41:04 PDT 2015
9             #===============================================================================
10              
11 1     1   660 use 5.008;
  1         3  
  1         35  
12 1     1   4 use strict;
  1         1  
  1         26  
13 1     1   4 use warnings;
  1         1  
  1         36  
14              
15             package Logwatch::RecordTree;
16 1     1   7429 use Moo;
  1         11867  
  1         6  
17 1     1   1219 use Carp qw( croak );
  1         2  
  1         63  
18 1     1   453 use UNIVERSAL::require;
  1         1148  
  1         8  
19 1     1   28 use List::Util qw ( max min sum );
  1         1  
  1         82  
20 1     1   477 use Sort::Key::Natural qw( natsort natkeysort );
  1         28514  
  1         95  
21              
22             our $VERSION = '2.056'; # VERSION
23              
24 1     1   8 use overload '""' => \&sprint;
  1         2  
  1         9  
25              
26             my $_defaults = {}; # class variable
27              
28             sub defaults {
29 96     96 1 110 my ($self) = @_;
30              
31 96   66     238 my $name = ref $self || $self;
32 96   100     224 $_defaults->{$name} ||= {}; # a hash for each sub-class
33 96         1981 return $_defaults->{$name};
34             }
35              
36             sub import {
37 1     1   9 my ($class, %hash) = @_;
38              
39 1         5 my $defaults = $class->defaults();
40 1         24 while (my ($key, $value) = each %hash) {
41 0         0 $defaults->{$key} = $value;
42             }
43             }
44              
45 221 50   221 0 6408 sub check_coderef { die 'Not a CODE ref' if (ref $_[0] ne 'CODE') };
46 99 50   99 0 2906 sub check_hashref { die 'Not a HASH ref' if (ref $_[0] ne 'HASH') };
47 167 50   167 0 6070 sub check_arryref { die 'Not an ARRAY ref' if (ref $_[0] ne 'ARRAY') };
48              
49             has name => ( # name/title for this item
50             is => 'ro',
51             );
52             has sprint_name => ( # callback to print the name
53             is => 'rw',
54             isa => \&check_coderef,
55             default => sub {
56             sub {
57             return $_[0]->name;
58             }
59             },
60             );
61             has sort_key => ( # this overrides ->name in sort_children
62             is => 'rw',
63             );
64             has case_sensitive => (
65             is => 'rw',
66             );
67             has count => ( # count how many times we log this event
68             is => 'rw',
69             default => sub { 0 },
70             trigger => sub {
71             $_[0]->no_count;
72             }
73             );
74             has count_fields => ( # fields to make the count
75             is => 'rw',
76             isa => \&check_arryref,
77             default => sub { [] },
78             );
79             has count_formatted => ( # count and extended fields, after formatting
80             is => 'rw',
81             );
82             has no_count => ( # suppress count field (probably because it's the same as the parent)
83             is => 'rw',
84             );
85             has children => ( # a hash of child Logwtch::RecordTrees
86             is => 'rw',
87             isa => \&check_hashref,
88             default => sub { {} },
89             );
90             has limit => ( # limit number of children printed
91             is => 'rw',
92             default => sub { 0 }, # default to no limit
93             );
94             has indent => ( # how much to indent this level of the tree
95             is => 'rw',
96             );
97             has no_indent => ( # flag to suppress indentation of children
98             is => 'rw',
99             );
100             has curr_indent => ( # total indentation of this level
101             is => 'rw',
102             default => sub { '' },
103             );
104             has post_callback => ( # when array is ready for printing, call this for final adjustments
105             is => 'rw',
106             isa => \&check_coderef,
107             default => sub { sub {} },
108             );
109             has lines => ( # when array is ready for printing, store a ref to it here
110             is => 'rw',
111             isa => \&check_arryref,
112             );
113             has columnize => ( # flag to indicate we should columnize children
114             is => 'rw',
115             );
116             has neat_names => ( # for the neat freaks
117             is => 'rw',
118             );
119             has neat_format => ( # formatter for neatness, set by sprint
120             is => 'rw',
121             default => sub { "%s" }, # not neat
122             );
123             has extra => ( # a little something extra...
124             is => 'rw',
125             );
126              
127             sub BUILD {
128 95     95 0 879 my ($self) = @_;
129              
130 95         96 while (my ($key, $value) = each %{$self->defaults}) {
  95         203  
131 0         0 $self->$key($value);
132             }
133             }
134              
135             sub child_by_name { # find child by name(s), follow down the tree
136 428     428 1 674 my ($self, @names) = @_;
137              
138 428         417 my $child = $self;
139 428         505 for my $name (@names) {
140 428 100       8292 return if (not exists $child->children->{$name});
141 334         7684 $child = $child->children->{$name};
142             }
143 334         2342 return $child;
144             }
145              
146             sub create_child { # create child, add to children
147 91     91 1 132 my ($self, $name, $type, $opts) = @_;
148              
149 91   100     276 $type ||= __PACKAGE__; # default to this package
150 91   100     257 $opts ||= {};
151 91         165 $opts->{name} = $name;
152 91 50       311 $type->require or croak($@);
153 91         2010 return $self->children->{$name} = $type->new( %{$opts} );
  91         2016  
154             }
155              
156             # adopt items, handle name conflicts
157             sub adopt {
158 6     6 1 13 my ($self, $item) = @_;
159              
160 6         27 my $item_name = $item->name;
161 6         14 my $my_child = $self->child_by_name($item_name);
162 6 100       46 if ($my_child) {
163             # name conflict. my_child must adopt $item's children
164 3         4 my @item_children = values %{$item->children};
  3         86  
165 3 100       31 if (@item_children) {
166 2         5 for my $child (@item_children) {
167 2         10 $my_child->adopt($child);
168             }
169             }
170             else {
171             # no children, so transfer count directly from item to my_child
172 1         33 $my_child->count($my_child->count + $item->count);
173             }
174             }
175             else {
176             # no name conflict, just copy over
177 3         80 $self->children->{$item_name} = $item;
178             }
179 6         188 $self->count($self->count + $item->count);
180             }
181              
182             # log event, add new children if necessaary
183             sub _log_children {
184 197     197   291 my ($self, $name, @children) = @_;
185              
186 197         138 my ($type, $opts);
187 197 100       363 if (ref $name eq 'ARRAY') {
188 42         35 ($name, $type, $opts) = @{$name};
  42         85  
189             }
190              
191 197 50       281 $name = "" if (not defined $name); # supposed to be a list of names or array-refs
192              
193 197         320 my $child = $self->child_by_name($name);
194 197 100       1512 if (not defined $child) {
195 91         198 $child = $self->create_child($name, $type, $opts)
196             }
197              
198 197 100       1777 if (@children) {
199 111         246 return $child->_log_children(@children);
200             }
201              
202 86         233 return $child;
203             }
204              
205             sub _count { # add 1 to count down the path
206 283     283   426 my ($self, $name, @children) = @_;
207              
208 283         5128 $self->count($self->count + 1);
209              
210 283 100       1048 $name = $name->[0] if (ref $name);
211 283 100       452 if (defined $name) {
212 197         325 return $self->child_by_name($name)->_count(@children);
213             }
214 86         375 return $self;
215             }
216              
217             sub log_no_count { # log new event without counting, add children if necessary
218 0     0 1 0 my ($self, @args) = @_;
219              
220 0         0 return $self->_log_children(@args);
221             }
222              
223             sub log { # log new event adding to count, add children if necessary
224 86     86 1 892 my ($self, @args) = @_;
225              
226 86         152 $self->_log_children(@args);
227 86         173 return $self->_count(@args);
228             }
229              
230             # return sorted list of child names
231             sub sort_children { # sort children
232 67     67 1 95 my ($self) = @_;
233              
234             # make hash, value is name, key is sort_key or name
235 61 100       472 my %keys = map { (defined($_->sort_key) ? $_->sort_key : $_->name) => $_ }
  67         1802  
236 67         68 values %{$self->children};
237              
238             # sort by hash keys, create array of values to get back to names
239 0         0 my @children = $self->case_sensitive
240 61         1734 ? map { $keys{$_} } natsort keys %keys
241 67 50   61   970 : map { $keys{$_} } natkeysort { lc $_ } keys %keys;
  61         5037  
242              
243             return wantarray
244             ? @children
245 67 50       437 : \@children;
246             }
247              
248             # make neat column of child names
249             sub _neaten_children {
250 2     2   4 my ($self) = @_;
251              
252 2         2 my $max = max(1, map { length $_->sprint_name->($_) } values %{$self->children});
  8         227  
  2         51  
253 2 50       26 my $format = $self->neat_names < 0
254             ? "%-${max}s"
255             : "%${max}s";
256 2         5 map { $_->neat_format($format) } values %{$self->children};
  8         54  
  2         68  
257             }
258              
259             # make neat columns of all the count fields
260             sub _format_child_counts {
261 77     77   594 my ($self, $children, $depth) = @_;
262              
263             # measure each field, save max length for each column
264 77         93 my @maxes;
265 77         79 for my $child (values %{$children}) {
  77         335  
266 77         80 unshift @{$child->count_fields}, $child->count;
  77         2000  
267 77         3343 my $ii = 0;
268 77         101 for my $field (@{$child->count_fields}) {
  77         2056  
269 83   100     858 $maxes[$ii] = max($maxes[$ii] || 0, length $field);
270 83         254 $ii++;
271             }
272             }
273              
274             # string to indent children: total count field width or at least 3
275 77         254 my $min = sum(1, @maxes);
276 77 100       228 $min = 3 if ($min < 3);
277 77         136 my $child_indent = " " x $min;
278              
279             # pad all fields to the max for the column
280 77         74 for my $child (values %{$children}) {
  77         217  
281 77         2273 my $ccf = $child->count_fields;
282 77         461 my @padded;
283 77         180 for my $ii (0 .. $#maxes) {
284 83   50     520 $padded[$ii] = sprintf "%*s", $maxes[$ii], $ccf->[$ii] || '';
285             }
286 77         356 $child->count_formatted(join '', @padded);
287 77 100       287 $child->indent($child_indent) if (not defined $child->indent);
288 77         74 shift @{$child->count_fields}; # remove the count field we inserted above
  77         1980  
289             }
290             }
291              
292             # compare our count fields to $other's (for suppression when identical)
293             sub _count_fields_differ {
294 13     13   22 my ($self, $other) = @_;
295              
296 13 50       397 return 1 if ($self->count != $other->count);
297 13         152 for my $ii (0 .. max($#{$self->count_fields}, $#{$other->count_fields})) {
  13         361  
  13         447  
298 1 50 33     35 return 1 if (
      33        
299             not defined $self->count_fields->[$ii] or
300             not defined $other->count_fields->[$ii] or
301             $self->count_fields->[$ii] ne $other->count_fields->[$ii]);
302             }
303 12         173 return 0; # match
304             }
305              
306             sub sprint {
307 70     70 1 146 my ($self, $callback, $path, $parent_indent, $depth) = @_;
308              
309 70   100     187 $path ||= [];
310 70   100     258 $parent_indent ||= '';
311 70   100     130 $depth ||= 1;
312              
313 70 100       167 if ($depth == 1) {
314             # top level needs to format its own count field
315 7         32 $self->_format_child_counts({ top => $self }, 0);
316             }
317              
318 70 100       361 my $count = $self->no_count
319             ? ''
320             : $self->count_formatted . ' ';
321              
322 70         2198 $self->lines(my $lines = []);
323              
324 70 50       982 if (length($self->name)) {
325 70         77 push @{$lines}, join( '',
  70         2024  
326             $count,
327             sprintf $self->neat_format, $self->sprint_name->($self),
328             );
329             }
330             else {
331 0         0 push @{$lines}, ''; # name is blank, so don't add anything here
  0         0  
332             }
333              
334             # format count fields and calculate indent for all children,
335 70         1907 $self->_format_child_counts($self->children, $depth);
336              
337 70         385 my $children = $self->sort_children;
338 70         74 my $child_count = @{$children};
  70         169  
339              
340 70 100       162 if ($child_count == 1) { # join single child to this line
341 13         21 my $child = $children->[0];
342             # save the child's flags we're going to alter
343 13         23 my %flags = map { $_ => $child->$_ } qw( no_count no_indent );
  26         116  
344             # suppress count field if child's is same as ours
345 13 100       52 $child->no_count(1) if (not $self->_count_fields_differ($child));
346 13         43 $child->no_indent(1);
347 13         42 $child->curr_indent($parent_indent); # no extra indent since we concat this line
348              
349 13         15 push @{$path}, $child->name;
  13         45  
350 13         63 $child->sprint($callback, $path, $self->curr_indent, $depth + 1);
351 13 50       373 if (length($lines->[0]) + length($child->lines->[0]) <= $self->width) {
352 13         28 $lines->[0] .= ' ' . shift @{$child->lines};
  13         371  
353             }
354 13         112 push @{$lines}, @{$child->lines};
  13         25  
  13         350  
355 13         84 pop @{$path};
  13         24  
356              
357             # restore child's flags
358 13         50 map { $child->$_($flags{$_}) } keys %flags;
  26         105  
359             }
360              
361 70 100       164 if ($child_count > 1) {
362 10   66     71 my $last = $self->limit || $child_count; # if limit is zero, print all
363 10 100       30 $last = $child_count if ($child_count - $last < 3); # if within 3, just print all
364 10         14 $last = $child_count - $last; # convert from limit to the last index
365              
366             # handle neat_children flag
367 10 100       47 $self->_neaten_children if ($self->neat_names);
368              
369 10         16 for my $child (@{$children}) {
  10         31  
370 51         276 $child->curr_indent($parent_indent . $self->indent);
371 51 100       111 if ($child_count <= $last) {
372 1         4 push @{$lines}, "... and $child_count more";
  1         5  
373 1         4 last;
374             }
375 50         53 push @{$path}, $child->name;
  50         175  
376 50         261 $child->sprint($callback, $path, $self->curr_indent, $depth + 1);
377 50         71 push @{$lines}, @{$child->lines};
  50         149  
  50         1435  
378 50         373 pop @{$path};
  50         81  
379 50         105 $child_count--;
380             }
381             }
382              
383 70 100       211 $self->sprint_columns if ($self->columnize);
384              
385             # indent children
386 70 100       210 if (not $self->no_indent) {
387 57         58 for my $ii (1 .. $#{$lines}) {
  57         188  
388 90         216 $lines->[$ii] = $self->indent . $lines->[$ii];
389             }
390             }
391              
392 70         2057 $self->post_callback->($self, $path);
393              
394 70 100       265 $callback->($self, $path) if ($callback);
395              
396 70         541 return join "\n", @{$lines};
  70         330  
397             }
398              
399             sub width {
400 15     15 1 126 my ($self, $new) = @_;
401              
402 15 50       46 if (@_ > 1) {
403 0         0 $self->{_width} = $new;
404             }
405 15 50       42 return $self->{_width} if (exists $self->{_width});
406 15         50 return 80; # default page width
407             }
408              
409             sub col_width {
410 4     4 1 7 my ($self, $new) = @_;
411              
412 4 50       12 if (@_ > 1) {
413 0         0 $self->{_col_width} = $new;
414             }
415              
416 4 100       10 if (not exists $self->{_col_width}) {
417             # find longest line length, excluding the first line
418 2         51 my $lines = $self->lines;
419 2         21 $self->{_col_width} = max( 1, map { length $lines->[$_] } (1 .. $#{$lines}));
  21         35  
  2         6  
420             }
421 4         20 return $self->{_col_width};
422             }
423              
424             sub col_count {
425 2     2 1 5 my ($self, $new) = @_;
426              
427 2 50       6 if (@_ > 1) {
428 0         0 $self->{_col_count} = $new;
429             }
430              
431 2 50       8 if (not exists $self->{_col_count}) {
432             # calculate from curr_indent, width, and col_width
433 2         11 my $width = $self->width - length($self->curr_indent);
434 2         8 $width = max(0, $width); # at least 0
435 2   50     8 $self->{_col_count} = int ($width / $self->col_width) || 1; # at least 1
436             }
437 2         6 return $self->{_col_count};
438             }
439              
440             # re-arrange children into columns
441             sub sprint_columns {
442 2     2 1 5 my ($self, $width, $col_count, $col_width) = @_;
443              
444             # allow caller to override width, col_count, and col_width
445 2 50       7 $self->width($width) if (defined $width);
446 2 50       30 $self->col_count($col_count) if (defined $col_count);
447 2 50       7 $self->col_width($col_width) if (defined $col_width);
448              
449 2         15 $col_count = $self->col_count;
450 2         4 $col_width = $self->col_width;
451              
452 2         54 my $lines = $self->lines;
453 2         12 my @new_lines = shift @{$lines}; # first line is unchanged
  2         8  
454              
455 2         3 my $lines_per_col = int ((@{$lines} + $col_count - 1) / $col_count);
  2         10  
456              
457 2         7 for my $ii (0 .. $lines_per_col - 1) {
458 4         5 my @line;
459 4 50       9 if ($col_count <= 1) {
460             # single column, just prepend indent
461 0         0 push @new_lines, join '',
462             $self->indent,
463             $lines->[$ii];
464             }
465             else {
466             # join segments into a line of columns
467 4         11 for my $jj (0 .. $col_count - 1) {
468 27         37 my $l = $lines->[$ii + $jj * $lines_per_col];
469 27 100       86 push @line, sprintf "%-*s", $col_width, $l if (defined $l);
470             }
471 4         29 push @new_lines, join '',
472             $self->indent,
473             join(' ', @line);
474             }
475             }
476 2         63 $self->lines(\@new_lines);
477             }
478              
479             1;
480              
481             __END__