File Coverage

blib/lib/Config/Model/ListId.pm
Criterion Covered Total %
statement 242 255 94.9
branch 77 100 77.0
condition 7 13 53.8
subroutine 41 42 97.6
pod 19 22 86.3
total 386 432 89.3


line stmt bran cond sub pod time code
1             #
2             # This file is part of Config-Model
3             #
4             # This software is Copyright (c) 2005-2022 by Dominique Dumont.
5             #
6             # This is free software, licensed under:
7             #
8             # The GNU Lesser General Public License, Version 2.1, February 1999
9             #
10             package Config::Model::ListId 2.153; # TRIAL
11              
12 29     29   544 use 5.10.1;
  29         133  
13 29     29   194 use Mouse;
  29         61  
  29         278  
14              
15 29     29   15596 use Config::Model::Exception;
  29         71  
  29         1144  
16 29     29   185 use Log::Log4perl qw(get_logger :levels);
  29         66  
  29         309  
17              
18 29     29   4686 use Carp;
  29         74  
  29         44309  
19             extends qw/Config::Model::AnyId/;
20              
21             with "Config::Model::Role::Grab";
22             with "Config::Model::Role::ComputeFunction";
23              
24             my $logger = get_logger("Tree::Element::Id::List");
25             my $user_logger = get_logger("User");
26              
27             has data => (
28             is => 'rw',
29             isa => 'ArrayRef',
30             default => sub { []; },
31             traits => ['Array'],
32             handles => {
33             _sort_data => 'sort_in_place',
34             _all_data => 'elements',
35             _splice_data => 'splice',
36             } );
37              
38             # compatibility with HashId
39             has index_type => ( is => 'ro', isa => 'Str', default => 'integer' );
40             has auto_create_ids => ( is => 'rw' );
41              
42             sub BUILD {
43 298     298 1 535 my $self = shift;
44              
45 298         638 foreach my $wrong (qw/max_nb min_index default_keys/) {
46             Config::Model::Exception::Model->throw(
47             object => $self,
48             error => "Cannot use $wrong with " . $self->get_type . " element"
49 894 50       1966 ) if defined $self->{$wrong};
50             }
51              
52 298 50       755 if ( defined $self->{migrate_keys_from} ) {
53 0         0 $user_logger->warn(
54             $self->name, "Using migrate_keys_from with ",
55             "list element is deprecated. Use migrate_values_from"
56             );
57             }
58              
59             # Supply the mandatory parameter
60 298         2506 return $self;
61             }
62              
63             sub set_properties {
64 301     301 0 526 my $self = shift;
65              
66 301         1427 $self->SUPER::set_properties(@_);
67              
68             # remove unwanted items
69 298         563 my $data = $self->{data};
70              
71 298 100       894 return unless defined $self->{max_index};
72              
73             # delete entries that no longer fit the constraints imposed by the
74             # warp mechanism
75 1         2 foreach my $k ( 0 .. $#{$data} ) {
  1         5  
76 0 0       0 next unless $k > $self->{max_index};
77 0         0 $logger->trace( "set_properties: ", $self->name, " deleting index $k" );
78 0         0 delete $data->[$k];
79             }
80             }
81              
82             sub _migrate {
83 1689     1689   2762 my $self = shift;
84              
85 1689 100       4244 return if $self->{migration_done};
86              
87             # migration must be done *after* initial load to make sure that all data
88             # were retrieved from the file before migration.
89 568 100       4808 return if $self->instance->initial_load;
90              
91 291         731 $self->{migration_done} = 1;
92              
93 291 100       1158 if ( $self->{migrate_values_from} ) {
    50          
94 2         11 my $followed = $self->safe_typed_grab( param => 'migrate_values_from', check => 'no' );
95 2 50       5 $logger->debug( $self->name, " migrate values from ", $followed->name )
96             if $logger->is_debug;
97 2         14 my $idx = $self->fetch_size;
98 2         7 foreach my $item ( $followed->fetch_all_indexes ) {
99 5         16 my $data = $followed->fetch_with_id($item)->dump_as_data( check => 'no' );
100 5         20 $self->fetch_with_id( $idx++ )->load_data($data);
101             }
102             }
103             elsif ( $self->{migrate_keys_from} ) {
104              
105             # FIXME: remove this deprecated stuff
106 0         0 my $followed = $self->safe_typed_grab( param => 'migrate_keys_from', check => 'no' );
107 0         0 for ( $followed->fetch_all_indexes ) {
108 0 0       0 $self->_store( $_, undef ) unless $self->_defined($_);
109             }
110             }
111              
112             }
113              
114             sub get_type {
115 786     786 1 1374 my $self = shift;
116 786         1775 return 'list';
117             }
118              
119             sub get_info {
120 2     2 1 6 my $self = shift;
121              
122 2         9 my @items = (
123             'type: ' . $self->get_type,
124             'index: ' . $self->index_type,
125             'cargo: ' . $self->cargo_type,
126             );
127              
128 2 100       7 if ( $self->cargo_type eq 'node' ) {
129 1         8 push @items, "cargo class: " . $self->config_class_name;
130             }
131              
132 2 100       7 if ( $self->cargo_type eq 'leaf' ) {
133 1   50     9 push @items, "leaf value type: " . ( $self->get_cargo_info('value_type') || '' );
134             }
135              
136 2         8 foreach my $what (qw/min_index max_index/) {
137 4         29 my $v = $self->$what();
138 4         9 my $str = $what;
139 4         16 $str =~ s/_/ /g;
140 4 50       13 push @items, "$str: $v" if defined $v;
141             }
142              
143 2         13 return @items;
144             }
145              
146             # important: return the actual size (not taking into account auto-created stuff)
147             sub fetch_size {
148 938     938 1 1651 my $self = shift;
149 938         1311 return scalar @{ $self->{data} };
  938         2336  
150             }
151              
152             sub _fetch_all_indexes {
153 1615     1615   2577 my $self = shift;
154 1615         2463 my $data = $self->{data};
155 1615 100       6116 return scalar @$data ? ( 0 .. $#$data ) : ();
156             }
157              
158             # fetch without any check
159             sub _fetch_with_id {
160 2483     2483   4604 my ( $self, $idx ) = @_;
161 2483         10494 return $self->{data}[$idx];
162             }
163              
164             sub load {
165 75     75 1 7285 my ( $self, $string, %args ) = @_;
166 75         300 my $check = $self->_check_check( $args{check} ); # I write too many checks.
167              
168 75         158 my @set;
169 75         133 my $cmd = $string;
170 75         257 $logger->debug( "load: ", $self->name, " called with ->$string<-" );
171              
172 75         822 my $regex = qr/^(
173             (?:
174             "
175             (?: \\" | [^"] )*?
176             "
177             )
178             |
179             [^,]+
180             )
181             /x;
182              
183 75         265 while ( length($string) ) {
184 221 100       1359 $string =~ s/$regex// or last;
185 219         602 my $tmp = $1;
186              
187 219 50       696 $tmp =~ s/^"|"$//g if defined $tmp;
188 219 50       500 $tmp =~ s/\\"/"/g if defined $tmp;
189 219         434 push @set, $tmp;
190              
191 219 100       497 last unless length($string);
192             }
193             continue {
194 146 50       619 $string =~ s/^,// or last;
195             }
196              
197 75 100       201 if ( length($string) ) {
198 2         47 Config::Model::Exception::Load->throw(
199             object => $self,
200             command => $cmd,
201             message => "unexpected load command '$cmd', left '$cmd'"
202             );
203             }
204              
205 73         248 $self->store_set(\@set, check => $check);
206             }
207              
208             sub store_set {
209 100     100 1 304 my $self = shift;
210 100         200 my (@v, %args);
211              
212 100 100       359 if (ref $_[0] eq 'ARRAY') {
213 74         151 @v = @{ shift @_ };
  74         241  
214 74         214 %args = @_;
215             }
216             else {
217 26         72 %args = ( check => 'yes' );
218 26         73 @v = @_;
219             }
220              
221 100 100       312 if ($logger->is_debug) {
222 29     29   295 no warnings "uninitialized";
  29         89  
  29         71817  
223 3         17 $logger->debug($self->name, " store_set called with ".map {"«$_» "} @v);
  7         26  
224             }
225              
226 100 50       727 my @comments = @{ $args{comment} || [] };
  100         544  
227 100         222 my $idx = 0;
228 100         231 foreach my $value (@v) {
229 298         1181 my $v_obj = $self->fetch_with_id( $idx++ );
230 298         1243 $v_obj->store( %args, value => $value );
231 297 50       934 $v_obj->annotation( shift @comments ) if @comments;
232             }
233              
234             # and delete unused items
235 99         420 $self->_prune_above_idx($idx);
236             }
237              
238             sub _prune_above_idx {
239 200     200   502 my ($self, $idx) = @_;
240             # and delete unused items
241 200         404 my $ref = $self->{data};
242 200         1316 while (scalar @$ref > $idx) {
243 38         139 $logger->debug($self->name, " pruning idx ", $#$ref);
244 38         406 $self->delete($#$ref);
245             }
246             }
247              
248             # store without any check
249             sub _store {
250 588     588   1307 my ( $self, $idx, $value ) = @_;
251 588         1675 return $self->{data}[$idx] = $value;
252             }
253              
254             sub _defined {
255 4984     4984   8726 my ( $self, $key ) = @_;
256 4984 50       18950 croak "argument '$key' is not numeric" unless $key =~ /^\d+$/;
257 4984         19220 return defined $self->{data}[$key];
258             }
259              
260             sub _exists {
261 644     644   1247 my ( $self, $idx ) = @_;
262 644         2082 return exists $self->{data}[$idx];
263             }
264              
265             sub _delete {
266 44     44   93 my ( $self, $idx ) = @_;
267 44         108 return delete $self->{data}[$idx];
268             }
269              
270             sub _clear {
271 13     13   33 my ($self) = @_;
272 13         427 $self->{data} = [];
273             }
274              
275             sub move {
276 2     2 1 794 my ( $self, $from, $to, %args ) = @_;
277 2         10 my $check = $self->_check_check( $args{check} );
278              
279 2         8 my $moved = $self->fetch_with_id($from);
280 2         12 $self->_delete($from);
281 2         6 delete $self->{warning_hash}{$from};
282              
283 2         6 my $ok = $self->check_idx($to);
284 2 50 33     16 if ( $ok or $check eq 'no' ) {
285 2         7 $self->_store( $to, $moved );
286 2         17 $moved->index_value($to);
287 2         19 $self->notify_change( note => "moved from index $from to $to" );
288 2         9 my $imode = $self->instance->get_data_mode;
289 2         7 $self->set_data_mode( $to, $imode );
290             }
291             else {
292             # restore moved item where it came from
293 0         0 $self->_store( $from, $moved );
294 0 0       0 if ( $check ne 'skip' ) {
295             Config::Model::Exception::WrongValue->throw(
296 0         0 error => join( "\n\t", @{ $self->{error} } ),
  0         0  
297             object => $self
298             );
299             }
300             }
301             }
302              
303             # list only methods
304             sub push {
305 19     19 1 98 my $self = shift;
306 19         69 $self->_assert_leaf_cargo;
307 19         77 my $idx = $self->fetch_size;
308 19         66 map { $self->fetch_with_id( $idx++ )->store($_); } @_;
  26         108  
309             }
310              
311             # list only methods
312             sub push_x {
313 2     2 1 4 my $self = shift;
314 2         9 my %args = @_;
315 2         7 $self->_assert_leaf_cargo;
316 2   50     7 my $check = delete $args{check} || 'yes';
317 2   33     9 my $v_arg = delete $args{values} || delete $args{value};
318 2 100       8 my @v = ref($v_arg) ? @$v_arg : ($v_arg);
319 2         4 my $anno = delete $args{annotation};
320 2 50       7 my @a = ref($anno) ? @$anno : $anno ? ($anno) : ();
    100          
321              
322 2 50       5 croak( "push_x: unexpected parameter ", join( ' ', keys %args ) ) if %args;
323              
324 2         5 my $idx = $self->fetch_size;
325 2         5 while (@v) {
326 3         6 my $val = shift @v;
327 3         12 my $obj = $self->fetch_with_id( $idx++ );
328 3         10 $obj->store($val);
329 3 100       22 $obj->annotation( shift @a ) if @a;
330             }
331             }
332              
333             sub unshift {
334 3     3 1 14 my $self = shift;
335 3         13 $self->insert_at( 0, @_ );
336             }
337              
338             sub insert_at {
339 22     22 1 44 my $self = shift;
340 22         34 my $idx = shift;
341              
342 22         63 $self->_assert_leaf_cargo;
343              
344             # check if max_idx is respected
345 22         69 $self->check_idx( $self->fetch_size + scalar @_ );
346              
347             # make room at the beginning of the array
348 22         112 $self->_splice_data( $idx, 0, (undef) x scalar @_ );
349 22         558 my $i = $idx;
350 22         49 map { $self->fetch_with_id( $i++ )->store($_); } @_;
  30         100  
351              
352 22         70 $self->_reindex;
353             }
354              
355             sub insert_before {
356 5     5 1 28 my $self = shift;
357 5         10 my $val = shift;
358             my $test =
359             ref($val) eq 'Regexp'
360 10     10   55 ? sub { $_[0] =~ /$val/ }
361 5 100   4   37 : sub { $_[0] eq $val };
  4         19  
362              
363 5         19 $self->_assert_leaf_cargo;
364              
365 5         14 my $point = 0;
366 5         17 foreach my $v ( $self->fetch_all_values ) {
367 14 100       39 last if $test->($v);
368 9         18 $point++;
369             }
370              
371 5         23 $self->insert_at( $point, @_ );
372             }
373              
374             sub insort {
375 6     6 1 27 my $self = shift;
376 6         21 $self->_assert_leaf_cargo;
377 6         30 my @insert = sort @_;
378              
379 6         21 my $point = 0;
380 6         31 foreach my $v ( $self->fetch_all_values ) {
381 36   100     124 while ( @insert and $insert[0] lt $v ) {
382 13         42 $self->insert_at( $point++, shift @insert );
383             }
384 36         67 $point++;
385             }
386 6 100       47 $self->push(@insert) if @insert;
387             }
388              
389             sub store {
390 0     0 1 0 my $self = shift;
391 0         0 $self->push_x(@_);
392             }
393              
394             sub _assert_leaf_cargo {
395 60     60   95 my $self = shift;
396              
397 60         188 my $ct = $self->cargo_type;
398              
399 60 50       179 Config::Model::Exception::User->throw(
400             object => $self,
401             error => "Cannot call sort on list of $ct"
402             ) unless $ct eq 'leaf';
403             }
404              
405             sub sort_algorithm {
406 32     32 1 287 return sub { $_[0]->fetch cmp $_[1]->fetch; };
  6     6   41  
407             }
408              
409             sub sort {
410 6     6 1 842 my $self = shift;
411              
412 6         34 $self->_assert_leaf_cargo;
413 6         25 $self->_sort_data( $self->sort_algorithm );
414              
415 6         76 my $has_changed = $self->_reindex;
416 6 100       42 $self->notify_change( note => "sorted" ) if $has_changed;
417             }
418              
419             sub _reindex {
420 28     28   44 my $self = shift;
421              
422 28         46 my $i = 0;
423 28         39 my $has_changed = 0;
424 28         84 foreach my $o ( $self->_all_data ) {
425 173 50       551 next unless defined $o;
426 173 100       458 $has_changed = 1 if $o->index_value != $i;
427 173         653 $o->index_value( $i++ );
428             }
429 28         133 return $has_changed;
430             }
431              
432             sub swap {
433 2     2 1 781 my $self = shift;
434 2         6 my $ida = shift;
435 2         3 my $idb = shift;
436              
437 2         5 my $obja = $self->{data}[$ida];
438 2         4 my $objb = $self->{data}[$idb];
439              
440             # swap the index values contained in the objects
441 2         8 my $obja_index = $obja->index_value;
442 2         18 $obja->index_value( $objb->index_value );
443 2         11 $objb->index_value($obja_index);
444              
445             # then swap the objects
446 2         5 $self->{data}[$ida] = $objb;
447 2         6 $self->{data}[$idb] = $obja;
448              
449 2         13 $self->notify_change( note => "swapped index $ida and $idb" );
450             }
451              
452             #die "check index number after wap";
453              
454             sub remove {
455 21     21 1 50 my $self = shift;
456 21         37 my $idx = shift;
457              
458 21 100       131 Config::Model::Exception::User->throw(
459             object => $self,
460             error => "Non numeric index for list: $idx"
461             ) unless $idx =~ /^\d+$/;
462              
463 20         89 $self->delete_data_mode( index => $idx );
464 20         306 my $note = "removed idx $idx";
465 20 100       80 if ( $self->{cargo}{type} eq 'leaf' ) {
466 17         108 $note .= ' ("' . $self->fetch_summary($idx) . '")';
467             }
468 20         82 $self->notify_change(note => $note);
469 20         39 splice @{ $self->{data} }, $idx, 1;
  20         108  
470             }
471              
472             #internal
473             sub auto_create_elements {
474 301     301 0 523 my $self = shift;
475              
476 301         886 my $auto_nb = $self->auto_create_ids;
477 301 100       833 return unless defined $auto_nb;
478              
479 2         6 $logger->debug( $self->name, " auto-creating $auto_nb elements" );
480              
481 2 100       43 Config::Model::Exception::Model->throw(
482             object => $self,
483             error => "Wrong auto_create argument for list: $auto_nb"
484             ) unless $auto_nb =~ /^\d+$/;
485              
486 1         3 my $auto_p = $auto_nb - 1;
487              
488             # create empty slots
489 1 50       3 map { $self->{data}[$_] = undef unless defined $self->{data}[$_]; } ( 0 .. $auto_p );
  4         18  
490             }
491              
492             # internal
493             sub create_default {
494 1313     1313 0 2052 my $self = shift;
495              
496 1313 100       1853 return if @{ $self->{data} };
  1313         3710  
497              
498             # list is empty so create empty element for default keys
499 548         1580 my $def = $self->get_default_keys;
500              
501 548         1032 map { $self->{data}[$_] = undef } @$def;
  4         16  
502              
503 548         1641 $self->create_default_with_init;
504             }
505              
506             sub load_data {
507 102     102 1 9622 my $self = shift;
508 102 100       450 my %args = @_ > 1 ? @_ : ( data => shift );
509 102         265 my $raw_data = delete $args{data};
510 102         397 my $check = $self->_check_check( $args{check} );
511              
512             my $data =
513             ref($raw_data) eq 'ARRAY' ? $raw_data
514 102 100       893 : $args{split_reg} ? [ split $args{split_reg}, $raw_data ]
    100          
    100          
515             : defined $raw_data ? [$raw_data]
516             : undef;
517              
518 102 100       401 Config::Model::Exception::LoadData->throw(
519             object => $self,
520             message => "load_data called with non expected data. Expected array ref or scalar",
521             wrong_data => $raw_data,
522             ) unless defined $data;
523              
524 101         244 my $idx = 0;
525 101         646 $logger->info( "ListId load_data (", $self->location, ") will load idx ", "0..$#$data" );
526 101         1089 foreach my $item (@$data) {
527 215         703 my $obj = $self->fetch_with_id( $idx );
528             # increment idx only if the value was accepted. This allow to
529             # prune the array to the right size.
530 215         926 $idx += $obj->load_data( %args, data => $item );
531             }
532              
533             # and delete unused items
534 101         423 $self->_prune_above_idx($idx);
535             }
536              
537             __PACKAGE__->meta->make_immutable;
538              
539             1;
540              
541             # ABSTRACT: Handle list element for configuration model
542              
543             __END__
544              
545             =pod
546              
547             =encoding UTF-8
548              
549             =head1 NAME
550              
551             Config::Model::ListId - Handle list element for configuration model
552              
553             =head1 VERSION
554              
555             version 2.153
556              
557             =head1 SYNOPSIS
558              
559             See L<Config::Model::AnyId/SYNOPSIS>
560              
561             =head1 DESCRIPTION
562              
563             This class provides list elements for a L<Config::Model::Node>.
564              
565             =head1 CONSTRUCTOR
566              
567             ListId object should not be created directly.
568              
569             =head1 List model declaration
570              
571             See
572             L<model declaration section|Config::Model::AnyId/"Hash or list model declaration">
573             from L<Config::Model::AnyId>.
574              
575             =head1 Methods
576              
577             =head2 get_type
578              
579             Returns C<list>.
580              
581             =head2 fetch_size
582              
583             Returns the number of elements of the list.
584              
585             =head2 load
586              
587             Parameters: C<< (string, [ check => 'no' ] ) >>
588              
589             Store a set of values passed as a comma separated list of values.
590             Values can be quoted strings. (i.e C<"a,a",b> yields
591             C<('a,a', 'b')> list).
592              
593             C<check> can be yes, no or skip
594              
595             =head2 store_set
596              
597             Store a set of values (passed as list)
598              
599             If tinkering with check is required, use the following way :
600              
601             store_set ( \@v , check => 'skip' );
602              
603             =head2 move
604              
605             Parameters: C<< ( from_index, to_index, [ check => 'no' ) >>
606              
607             Move an element within the list. C<check> can be 'yes' 'no' 'skip'
608              
609             =head2 push
610              
611             Parameters: C<< ( value1, [ value2 ... ] ) >>
612              
613             push some values at the end of the list.
614              
615             =head2 push_x
616              
617             Parameters: C<< ( values => [ v1','v2', ...] , ... ) >>
618              
619             Like push with extended options. Options are:
620              
621             =over
622              
623             =item check
624              
625             Check value validaty. Either C<yes> (default), C<no>, C<skip>
626              
627             =item values
628              
629             Values to push (array_ref)
630              
631             =item value
632              
633             Single value to push
634              
635             =item annotation
636              
637             =back
638              
639             =head2 unshift
640              
641             Parameters: C<< ( value1, [ value2 ... ] ) >>
642              
643             unshift some values at the end of the list.
644              
645             =head2 insert_at
646              
647             Parameters: C<< ( idx, value1, [ value2 ... ] ) >>
648              
649             unshift some values at index idx in the list.
650              
651             =head2 insert_before
652              
653             Parameters: C<< ( ( val | qr/stuff/ ) , value1, [ value2 ... ] ) >>
654              
655             unshift some values before value equal to C<val> or before value matching C<stuff>.
656              
657             =head2 insort
658              
659             Parameters: C<< ( value1, [ value2 ... ] ) >>
660              
661             Insert C<zz> value on C<xxx> list so that existing alphanumeric order is preserved.
662             C<insort> yields unpexpected results when called on an unsorted list.
663              
664             =head2 store
665              
666             Equivalent to push_x. This method is provided to help write
667             configuration parser, so the call is the same when dealing with leaf or
668             list values. Prefer C<push_x> when practical.
669              
670             =over 4
671              
672             =item check
673              
674             C<yes>, C<no> or C<skip>
675              
676             =item annotation
677              
678             list ref of annotation to store with the list values
679              
680             =back
681              
682             Example:
683              
684             $elt->push_x (
685             values => [ 'v1','v2' ] ,
686             annotation => [ 'v1 comment', 'v2 comment' ],
687             check => 'skip'
688             );
689              
690             =head2 sort
691              
692             Sort the content of the list. Can only be called on list of leaf.
693              
694             =head2 swap
695              
696             Parameters: C<< ( ida , idb ) >>
697              
698             Swap 2 elements within the array
699              
700             =head2 remove
701              
702             Parameters: C<< ( idx ) >>
703              
704             Remove an element from the list. Equivalent to C<splice @list,$idx,1>
705              
706             =head2 load_data
707              
708             Parameters: C<< ( data => ( ref | scalar ) [, check => ... ] [ , split_reg => $re ] ) >>
709              
710             Clear and load list from data contained in the C<data> array ref. If a scalar or a hash ref
711             is passed, the list is cleared and the data is stored in
712             the first element of the list. If split_reg is specified, the scalar is split
713             to load the array.
714              
715             For instance
716              
717             $elt->load_data( data => 'foo,bar', split_reg => qr(,) ) ;
718              
719             loads C< [ 'foo','bar']> in C<$elt>
720              
721             =head2 sort_algorithm
722              
723             Returns a sub used to sort the list elements. See
724             L<perlfunc/sort>. Used only for list of leaves. This method can be
725             overridden to alter sort order.
726              
727             =head2 get_info
728              
729             Returns a list of information related to the list. See
730             L<Config::Model::Value/get_info> for more details.
731              
732             =head1 AUTHOR
733              
734             Dominique Dumont, (ddumont at cpan dot org)
735              
736             =head1 SEE ALSO
737              
738             L<Config::Model::Model>,
739             L<Config::Model::Instance>,
740             L<Config::Model::AnyId>,
741             L<Config::Model::HashId>,
742             L<Config::Model::Value>
743              
744             =head1 AUTHOR
745              
746             Dominique Dumont
747              
748             =head1 COPYRIGHT AND LICENSE
749              
750             This software is Copyright (c) 2005-2022 by Dominique Dumont.
751              
752             This is free software, licensed under:
753              
754             The GNU Lesser General Public License, Version 2.1, February 1999
755              
756             =cut