root/main/trunk/greenstone2/perllib/oaiinfo.pm @ 31192

Revision 31192, 21.5 KB (checked in by ak19, 3 years ago)

Replacing unnecessary functions and removing unused functions.

Line 
1# This class based on arcinfo.pm
2package oaiinfo;
3
4use constant INFO_STATUS_INDEX  => 0;
5use constant INFO_TIMESTAMP_INDEX => 1;
6
7use strict;
8
9use arcinfo;
10use dbutil;
11
12# QUESTIONS:
13# Should we use time or localtime(time) for timestamp? Just timestamp.
14# What format should the timestamp be in, or is the basic format used by perl sufficient? Basic.
15
16# File format read in: OID <tab> Date-timestamp <tab> Deletion-Status
17
18# Deletion status can be:
19#  E = Doc with OID exists (has not been deleted from collection). Timestamp indicates last time of build
20#  D = Doc with OID has been deleted. Timestamp indicates time of deletion
21#  PD = Provisionally Deleted. Timestamp momentarily unaltered.
22
23# oaidb is "always incremental": always reflects the I/B/R/D status of archive info db,
24# before the indexing step of the build phase that alters the I/B/R/D contents of archive info db.
25# (I=index, B=been indexed, R=reindex; D=delete)
26
27sub new {
28    my $class = shift(@_);
29    my ($config_filename, $infodbtype) = @_;
30 
31    my $self = {
32    'info'=>{} # map of {OID, array[deletion-status,timestamp]} pairs
33    };
34   
35    if(!defined $infodbtype) {
36    $infodbtype = &dbutil::get_default_infodb_type();
37    }
38    $infodbtype = "gdbm" if ($infodbtype eq "gdbm-txtgz");
39    $self->{'infodbtype'} = $infodbtype;
40
41    # Create and store the db filenames we'll be working with (tmp and livedb)
42    my $etc_dir = &util::get_parent_folder($config_filename);
43
44    my $perform_firsttime_init = 0;
45    $self->{'oaidb_live_filepath'} = &dbutil::get_infodb_file_path($infodbtype, "oai-inf", $etc_dir, $perform_firsttime_init);
46    $self->{'oaidb_tmp_filepath'} = &dbutil::get_infodb_file_path($infodbtype, "oai-inf-tmp", $etc_dir, $perform_firsttime_init);
47    $self->{'etc_dir'} = $etc_dir;
48#    print STDERR "############ LIVE DB: $self->{'oaidb_live_filepath'}\n";
49#    print STDERR "############ TMP DB: $self->{'oaidb_tmp_filepath'}\n";
50
51    $self->{'oaidb_file_path'} = $self->{'oaidb_tmp_filepath'}; # db file we're working with
52
53    return bless $self, $class;
54}
55
56# this subroutine will work out the starting contents of the tmp-db (temporary oai db):
57# whether it should start off empty, or with the contents of any existing live-db,
58# or with the contents of any existing tmp-db.
59sub init_tmpdb {
60    my $self = shift(@_);
61    my ($removeold, $have_manifest) = @_;
62
63    # if we have a manifest file, then we pretend we are fully incremental for oaiinfo db.
64    # removeold implies proper full-rebuild, whereas keepold or incremental means incremental
65    if($have_manifest) { # if we have a manifest file, we're not doing removeold/full-rebuild either
66    $removeold = 0;
67    }
68
69    my $do_pd_step = ($removeold) ? 1 : 0;
70       # if $removeold, then proper full rebuild, will carry out step where all E will be marked as PD
71       # else some kind of incremental build, won't do the extra PD pass
72       # which is the step marking existing OIDs (E) as PD (provisionally deleted) 
73   
74    my $oaidb_live_filepath = $self->{'oaidb_live_filepath'};
75    my $oaidb_tmp_filepath = $self->{'oaidb_tmp_filepath'};
76    my $infodbtype = $self->{'infodbtype'};
77    # Note: the live db can only exist if the collection has been activated at least once before
78    my $livedb_exists = &FileUtils::fileExists($oaidb_live_filepath);
79    my $tmpdb_exists = &FileUtils::fileExists($oaidb_tmp_filepath);   
80
81    my $initdb = 0;
82   
83    # work out what operation we need to do
84    #    work with empty tmpdb
85    #    copy_livedb_to_tmpdb
86    #    work with existing tmpdb (so existing tmpdb will be topped up)
87
88    # make_contents_of_tmpdb_empty
89    # make_contents_of_tmpdb_that_of_livedb
90    # continue_working_with_tmpdb ("contents_of_tmpdb_is_tmpdb")
91
92    # We're going to prepare the starting state of tmpdb next.
93    # It can start off empty, start off with the contents of livedb, or it can start off with the contents
94    # of the existing tmp db. Which of these three it is depends on the 3 factors: whether livedb exists,
95    # whether tmpdb exists and whether or not removeold is true.
96    # i.o.w. which of the 3 outcomes it is depends on the truth table built on the following 3 variables:
97    #   LDB = LiveDB exists
98    #   TDB = TmpDB exists
99    #   RO = Removeold
100    # OUTCOMES:
101    #   clean slate (create an empty tmpdb/make tmpdb empty)
102    #   top up tmpDB (work with existing tmpdb)
103    #   copy LiveDB to TmpDB (liveDB's contents become the contents of TmpDB, and we'll work with that)
104    #
105    # TRUTH TABLE:
106    # ---------------------------------------
107    # LDB TDB  RO | Outcome
108    # ---------------------------------------
109    #  0   0   0  | clean-slate
110    #  0   0   1  | clean-slate
111    #  0   1   0  | top-up-tmpdb
112    #  0   1   1  | erase tmpdb, clean-slate
113    #  1   0   0  | copy livedb to tmpdb
114    #  1   0   1  | copy livedb to tmpdb
115    #  1   1   0  | top-up-tmpdb
116    #  1   1   1  | copy livedb to tmpd
117    # ---------------------------------------
118    #
119    # Dr Bainbridge worked out using Karnaugh maps that, from the above truth table:
120    # => clean-slate/empty-tmpdb = !LDB && (RO || !TDB)
121    # => top-up-tmpdb/work-with-existing-tmpdb = !RO && TDB
122    # => copy-livedb-to-tmpdb = LDB && (!TDB || RO)
123    # I had most of these tests, except that I hadn't (yet) merged the two clean slate instances
124    # of first-build-ever and make-contents-of-tmpdb-empty
125
126    #my $first_build_ever = (!$livedb_exists && !$tmpdb_exists);
127    #my $make_contents_of_tmpdb_empty = (!$livedb_exists && $tmpdb_exists && $removeold);
128    # Karnaugh map allows merging $first_build_ever and $make_contents_of_tmpdb_empty above
129    # into: my $work_with_empty_tmpdb = (!$livedb_exists && (!$tmpdb_exists || $removeold));
130    my $work_with_empty_tmpdb = (!$livedb_exists && (!$tmpdb_exists || $removeold));
131    my $make_contents_of_tmpdb_that_of_livedb = ($livedb_exists && (!$tmpdb_exists || $removeold));
132    my $work_with_existing_tmpdb = ($tmpdb_exists && !$removeold);
133
134    if($work_with_empty_tmpdb) { # we'll use an empty tmpdb
135
136    # If importing the collection for the very first time, neither db exists,
137    # so create an empty tmpdb.
138    #
139    # We also create an empty tmpdb when livedb doesn't exist and $removeold is true.
140    # This can happen if we've never run activate (so no livedb),
141    # yet had done some import (and perhaps building) followed by a full re-import now.
142    # Since there was no activate and we're doing a removeold/full-rebuild now, can just
143    # work with a new tmpdb, even though one already existed, its contents can be wiped out.
144        # In such a scenario, we'll be deleting tmpdb. Then there  will be no livedb nor any tmpdb
145    # any more, so same situation as if importing the very first time when no oaidb exists either.
146
147    &dbutil::remove_db_file($self->{'infodbtype'}, $oaidb_tmp_filepath) if $tmpdb_exists; # remove the db file and any assoc files
148    $initdb = 1; # new tmpdb
149   
150    # if the oai db is created the first time, it's like incremental and
151    # "keepold" (keepold means "only add, don't reprocess existing"). So
152    # no need to do the special passes dealing with "provisional deletes".
153    $do_pd_step = 0;
154   
155    } elsif ($make_contents_of_tmpdb_that_of_livedb) {
156
157    # If the livedb exists and we're doing a full rebuild ($removeold is true),
158    # copy livedb to tmp regardless of if tmpdb already exists.
159    # Or if the livedb exists and tmpdb doesn't exist, it doesn't matter
160    # if we're incremental or not: also copy live to tmp and work with tmp.
161   
162    # copy livedb to tmpdb
163    &dbutil::remove_db_file($self->{'infodbtype'}, $oaidb_tmp_filepath) if $tmpdb_exists; # remove the db file and any assoc files
164    &FileUtils::copyFiles($oaidb_live_filepath, $oaidb_tmp_filepath);
165   
166    $initdb = 0; # tmpdb exists, since we just copied livedb to tmpdb, so need to init existing tmpdb
167
168    } else { # $work_with_existing_tmpdb, so we'll build on top of what's presently already in tmpdb
169         # (we'll be topping up the current tmpdb)
170
171    # !$removeold, meaning incremental
172    # If incremental and have a tmpdb already, regardless of whether livedb exists,
173    # then work with the existing tmpdb file, as this means we've been
174    # importing (perhaps followed by building) repeatedly without activating the
175    # last time but want to maintain the (incremental) changes in tmpdb.       
176     
177    $initdb = 0;
178
179    } # Dr Bainbridge drew up Karnaugh maps on the truth table, which proved that all cases
180                    # are indeed covered above, so don't need any other catch-all else here
181
182    $self->{'oaidb_file_path'} = &dbutil::get_infodb_file_path($infodbtype, "oai-inf-tmp", $self->{'etc_dir'}, $initdb);
183                                 # final param follows jmt's $perform_firsttime_init in inexport.pm
184
185#    print STDERR "@@@@@ oaidb: $self->{'oaidb_file_path'}\n";
186   
187    return $do_pd_step;
188}
189
190sub get_filepath {
191    my $self = shift (@_);
192    return $self->{'oaidb_file_path'};
193}
194
195sub import_stage {
196    my $self = shift (@_);
197    my ($removeold, $have_manifest) = @_;
198   
199    my $do_pd_step = $self->init_tmpdb($removeold, $have_manifest);
200      # returns 1 if the step to mark oaidb entries as PD is required
201      # if we're doing full rebuilding and it's NOT the first time creating the oai_inf db,
202      # then the tasks to do with PD (provisionally deleted) OAI OIDs should be carried out
203
204    $self->load_info();
205    $self->print_info(); # DEBUGGING
206
207    if ($do_pd_step) {
208    $self->mark_all_existing_as_provisionallydeleted();
209    $self->print_info(); # DEBUGGING
210
211    # save to db file now that we're done
212    $self->save_info();
213    }
214
215}
216
217sub building_stage_before_indexing() {
218    my $self = shift (@_);   
219    my ($archivedir) = @_;
220
221    # load archive info db into memory
222    my $arcinfo_doc_filename = &dbutil::get_infodb_file_path($self->{'infodbtype'}, "archiveinf-doc", $archivedir);
223    my $arcinfo_src_filename = &dbutil::get_infodb_file_path($self->{'infodbtype'}, "archiveinf-src", $archivedir);
224    my $archive_info = new arcinfo ($self->{'infodbtype'});
225    $archive_info->load_info ($arcinfo_doc_filename);
226
227    #my $started_from_scratch = &FileUtils::fileTest($self->{'oaidb_tmp_filepath'}, '-z'); # 1 if tmpdb is empty
228        # -z test for file is empty http://www.perlmonks.org/?node_id=927447
229   
230    # load the oaidb file's contents into memory.
231    $self->load_info();
232    $self->print_info(); # DEBUGGING
233
234    # process all the index, reindex and delete operations as indicated in arcinfo,
235    # all the while ensuring all PDs are changed back to E for OIDs that exist in both arcinfo and oaiinfo db. 
236
237    foreach my $OID (keys $archive_info->{'info'}) {
238    my $arcinf_tuple = $archive_info->{'info'}->{$OID};
239    my $indexing_status = $arcinf_tuple->[arcinfo::INFO_STATUS_INDEX];
240                 # use packageName::constant to refer to constants declared in another package,
241                 # see http://perldoc.perl.org/constant.html
242
243    print STDERR "######## OID: $OID - status: $indexing_status\n";
244
245    if($indexing_status eq "I") {
246        $self->index($OID); # add new as E with current timestamp/or set existing as E with orig timestamp
247    } elsif($indexing_status eq "R") {
248        $self->reindex($OID); # update timestamp and ensure marked as E (if oid doesn't exist, add new)
249    } elsif($indexing_status eq "D") {
250        $self->delete($OID); # set as D with current timestamp
251    } elsif($indexing_status eq "B") { # B for "been indexed"
252        $self->been_indexed($OID); # will flip any PD to E if oid exists, else will add new entry for oid
253        # A new entry may be required if the collection had been built prior to turning this into
254        # an oaicollection. But what if we always maintain an oaidb? Still call $self->index() here.
255    } else {
256        print STDERR "### oaiinfo::building_stage_before_indexing(): Unrecognised indexing status $indexing_status\n";
257    }
258    }
259
260    # once all docs processed, go through oaiiinfo db changing any PDs to D along with current timestamp
261    # to indicate that they're deleted
262    $self->mark_all_provisionallydeleted_as_deleted();
263    $self->print_info();
264   
265    # let's save to db file now that we're done
266    $self->save_info();
267   
268}
269
270sub activate_collection { # move tmp db to live db
271    my $self = shift (@_);
272
273    my $oaidb_live_filepath =  $self->{'oaidb_live_filepath'};
274    my $oaidb_tmp_filepath = $self->{'oaidb_tmp_filepath'};
275
276    my $livedb_exists = &FileUtils::fileExists($oaidb_live_filepath);
277    my $tmpdb_exists = &FileUtils::fileExists($oaidb_tmp_filepath);
278
279    if($tmpdb_exists) {
280    if($livedb_exists) {
281        #&dbutil::remove_db_file($self->{'infodbtype'}, s$oaidb_live_filepath); # remove the db file and any assoc files
282        &dbutil::rename_db_file_to($self->{'infodbtype'}, $oaidb_live_filepath, $oaidb_live_filepath.".bak"); # rename the db file and any assoc files
283    }
284    #&FileUtils::moveFiles($oaidb_tmp_filepath, $oaidb_live_filepath);
285    &dbutil::rename_db_file_to($self->{'infodbtype'}, $oaidb_tmp_filepath, $oaidb_live_filepath); # rename the db file and any assoc files
286
287    print STDERR "#### Should now have MOVED $self->{'oaidb_tmp_filepath'} to $self->{'oaidb_live_filepath'}\n";
288   
289    } else {
290    print STDERR "@@@@@ In oaiinfo::activate_collection():\n";
291    print STDERR "@@@@@   No tmpdb at $self->{'oaidb_tmp_filepath'}\n";
292    print STDERR "@@@@@   to make 'live' by moving to $self->{'oaidb_live_filepath'}.\n";
293    }
294}
295
296##################### SPECIFIC TO PD-STEP ####################
297
298
299# mark all existing, E (non-deleted) OIDs as Provisionally Deleted (PD)
300# this subroutine doesn't save to oai-inf.DB
301# the caller should call save_info when they want to save to the db
302sub mark_all_existing_as_provisionallydeleted {
303    my $self = shift (@_);
304   
305    print STDERR "@@@@@ oaiinfo::mark_all_E_as_PD(): Marking the E entries as PD\n";
306
307    foreach my $OID (keys $self->{'info'}) {
308    my $OID_info = $self->{'info'}->{$OID};
309    my $curr_status = $OID_info->[INFO_STATUS_INDEX];
310    if($curr_status eq "E") {       
311        $OID_info->[INFO_STATUS_INDEX] = "PD";
312    }
313    }
314}
315
316# mark all OIDs that are Provisionally Deleted (PD) as deleted, and set to current timestamp
317# To be called at end of build. Again, the caller should save to DB by calling save_info.
318sub mark_all_provisionallydeleted_as_deleted {
319    my $self = shift (@_);
320   
321    print STDERR "@@@@@ oaiinfo::mark_all_PD_as_D(): Marking the PD entries as D\n";
322
323    foreach my $OID (keys $self->{'info'}) {
324    my $OID_info = $self->{'info'}->{$OID};
325    my $curr_status = $OID_info->[INFO_STATUS_INDEX];
326    if($curr_status eq "PD") {
327        $self->set_info($OID, "D", $self->get_current_time());
328    }
329    }
330}
331
332
333##################### GENERAL, NOT SPECIFIC TO PD-STEP ####################
334
335sub print_info {
336    my $self = shift (@_);
337
338    print STDERR "###########################################################\n";
339    print STDERR "@@@@@ oaiinfo::print_info(): oaidb in memory contains: \n";
340   
341    foreach my $OID (keys $self->{'info'}) {
342    print STDERR "OID: $OID";
343    print STDERR " status: " . $self->{'info'}->{$OID}->[INFO_STATUS_INDEX];
344    print STDERR " time: " . $self->{'info'}->{$OID}->[INFO_TIMESTAMP_INDEX];
345    print STDERR "\n";
346    }
347
348    print STDERR "###########################################################\n";
349}
350
351
352# Find the OID, if it exists, make its status=E for existing. Leave its timestamp alone.
353# If the OID doesn't yet exist, add it as a new entry with status=E and with current timestamp.
354sub index { # Add a new oid with current time and E. If the oid was already present, mark as E
355    my $self = shift (@_);
356    my ($OID) = @_;
357   
358    my $OID_info = $self->{'info'}->{$OID};
359   
360    if (defined $OID_info) { # if OID is present, this will change status back to E, timestamp unchanged
361    $OID_info->[INFO_STATUS_INDEX] = "E";
362   
363    } else { # if OID is not present, then it's now added as existing from current time on
364    $self->set_info($OID, "E", $self->get_current_time());
365    }
366}
367
368# Upon reindexing a document with identifier OID, change its timestamp to current time
369# if a new OID, then add as new entry with status=E and current timestamp
370sub reindex { # update timestamp if oid is already present, if not (unlikely), add as new
371    my $self = shift (@_);
372    my ($OID) = @_;
373
374    my $OID_info = $self->{'info'}->{$OID};   
375    $self->set_info($OID, "E", $self->get_current_time()); # Takes care of 3 things:
376       # if OID exists, updates modified time to indicate the doc has been reindexed
377       # if OID exists, ensures any status=PD is flipped back to E for this OID doc (as we know it exists);
378       # if the OID doesn't yet exist, adds a new OID entry with status=E and current timestamp.
379
380}
381
382# Does the same as index():
383# OIDs that have been indexed upon rebuild may still be new to the oaidb: GS2 collections
384# are not OAI collections by default, unlike GS3 collections. Imagine rebuilding a (GS2) collection
385# 5 times and then setting them to be an OAI collection. In that case, the doc OIDs in the collection
386# may not be in the oaidb yet. Unless, we decide (as is the present case) to always maintain an oaidb
387# (always creating an oaidb regardless of whether the collection has OAI support turned on or not).
388sub been_indexed {
389    my $self = shift (@_);
390    my ($OID) = @_;
391
392    $self->index($OID);
393}
394
395# Upon deleting a document with identifier OID,
396# set status to deleted and change its timestamp to current time
397sub delete {
398    my $self = shift (@_);
399    my ($OID) = @_;
400
401    # the following method will set to current time if no timestamp provided,
402    # But by explicit here, the code is easier to follow
403    $self->set_info($OID, "D", $self->get_current_time());
404
405}
406
407#############################################################
408sub get_current_time {
409    my $self = shift (@_);
410    return time; # current time
411
412    # localtime(time) returns an array of values (day, month, year, hour, min, seconds) or singular string
413    # return localtime; # same as localtime(time); # http://perldoc.perl.org/functions/localtime.html
414   
415}
416
417sub _load_info_txt
418{
419    my $self = shift (@_);
420    my ($filename) = @_;
421
422    if (defined $filename && &FileUtils::fileExists($filename)) {
423    open (INFILE, $filename) ||
424        die "oaiinfo::load_info couldn't read $filename\n";
425
426    my ($line, @lineparts);
427    while (defined ($line = <INFILE>)) {
428        $line =~ s/\cM|\cJ//g; # remove end-of-line characters
429        @lineparts = split ("\t", $line);
430        if (scalar(@lineparts) >= 2) {
431        $self->set_info (@lineparts);
432        }
433    }
434    close (INFILE);
435    }
436
437}
438
439sub _load_info_db
440{
441    my $self = shift (@_);
442    my ($filename) = @_;
443
444    my $infodb_map = {};
445
446    &dbutil::read_infodb_file($self->{'infodbtype'}, $filename, $infodb_map);
447
448    foreach my $oid ( keys $infodb_map ) {
449    my $vals = $infodb_map->{$oid};
450    # interested in oid, timestamp, deletion status
451
452    my ($deletion_status) = ($vals=~/^<status>(.*)$/m);
453    my ($timestamp) = ($vals=~/^<timestamp>(.*)$/m);
454   
455    $self->set_info ($oid, $deletion_status, $timestamp);
456    }
457}
458
459# if no filename is passed in (and you don't generally want to), then
460# it tries to load in <collection>/etc/oai-inf.<db> if it exists
461sub load_info {
462    my $self = shift (@_);
463    my ($filename) = @_;
464
465    $self->{'info'} = {};
466
467    $filename = $self->{'oaidb_file_path'} unless defined $filename;
468
469    if (&FileUtils::fileExists($filename)) {
470    if ($filename =~ m/\.inf$/) {
471        $self->_load_info_txt($filename);
472    }
473    else {
474        $self->_load_info_db($filename);
475    }
476    }
477
478}
479
480sub _save_info_txt {
481    my $self = shift (@_);
482    my ($filename) = @_;
483
484    my ($OID, $info);
485
486    open (OUTFILE, ">$filename") ||
487    die "oaiinfo::save_info couldn't write $filename\n";
488 
489    foreach $info (@{$self->get_OID_list()}) {
490    if (defined $info) {
491        print OUTFILE join("\t", @$info), "\n";
492    }
493    }
494    close (OUTFILE);
495}
496
497# if no filename is passed in (and you don't generally want to), then
498# this subroutine tries to write to <collection>/etc/oai-inf.<db>.
499sub _save_info_db {
500    my $self = shift (@_);
501    my ($filename) = @_;
502
503    $filename = $self->{'oaidb_file_path'} unless defined $filename;
504    my $infodbtype = $self->{'infodbtype'};
505
506    # write out again. Open file for overwriting, not appending.
507    # Then write out data structure $self->{'info'} that's been maintaining the data in-memory.
508    my $infodb_handle = &dbutil::open_infodb_write_handle($infodbtype, $filename);
509    foreach my $oid ( keys $self->{'info'} ) {
510    my $OID_info = $self->{'info'}->{$oid};
511    #my $val_hash = {
512    #    "status" => $OID_info->[INFO_STATUS_INDEX],
513    #    "timestamp" => $OID_info->[INFO_TIMESTAMP_INDEX]
514    #};
515   
516    #&dbutil::write_infodb_rawentry($infodbtype,$infodb_handle,$oid,$val_hash);
517
518    my $val = "<status>".$OID_info->[INFO_STATUS_INDEX]."\n<timestamp>".$OID_info->[INFO_TIMESTAMP_INDEX]."\n";
519    &dbutil::write_infodb_rawentry($infodbtype,$infodb_handle,$oid,$val);
520    }
521    &dbutil::close_infodb_write_handle($infodbtype, $infodb_handle);
522}
523
524sub save_info {
525    my $self = shift (@_);
526    my ($filename) = @_;
527
528    if(defined $filename) {
529    if ($filename =~ m/(contents)|(\.inf)$/) {
530        $self->_save_info_txt($filename);
531    }
532    else {
533        $self->_save_info_db($filename);
534    }
535    } else {
536    $self->_save_info_db();
537    }
538}
539
540
541sub set_info { # sets existing or appends
542    my $self = shift (@_);
543    my ($OID, $del_status, $timestamp) = @_;
544    if(!defined $timestamp) { # get current date timestamp
545    $timestamp = $self->get_current_time();
546    }
547    $self->{'info'}->{$OID} = [$del_status, $timestamp];
548
549}
550
551
552# returns a list of the form [[OID, timestamp, deletion_status], ...]
553sub get_OID_list
554{
555    my $self = shift (@_);
556
557    my @list = ();
558
559    foreach my $OID (keys $self->{'info'}) {   
560    my $OID_info = $self->{'info'}->{$OID};
561
562    push (@list, [$OID, $OID_info->[INFO_STATUS_INDEX],
563              $OID_info->[INFO_TIMESTAMP_INDEX]]);
564    }
565
566    return \@list;
567}
568
569
570# returns the number of entries so far, including deleted ones
571# http://stackoverflow.com/questions/1109095/how-can-i-find-the-number-of-keys-in-a-hash-in-perl
572sub size {
573    my $self = shift (@_);
574    return (scalar keys $self->{'info'});
575}
576
5771;
Note: See TracBrowser for help on using the browser.