File Coverage

blib/lib/XML/Tape/Index.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             #
2             # $Id: Index.pm,v 1.5 2005/09/01 08:19:27 patrick Exp $
3             #
4              
5             =head1 NAME
6              
7             XML::Tape::Index - a XMLtape indexer
8              
9             =head1 SYNOPSIS
10              
11             use XML::Tape::Index qw(:all);
12              
13             unless (indexexists('ex/tape.xml')) {
14             $x = indexopen('ex/tape.xml', 'w');
15             $x->reindex;
16             $x->indexclose();
17             }
18              
19             $x = indexopen('ex/tape.xml', 'r');
20              
21             for (my $rec = $x->list_identifiers();
22             defined($rec);
23             $rec = $x->list_identifiers($rec->{token})) {
24             print "id : %s\n" , $rec->{identifier};
25             print "date : %s\n" , $rec->{date};
26             print "start : %s\n" , $rec->{start};
27             print "length : %s\n" , $rec->{len};
28             }
29              
30             my $rec = $x->get_identifier('oai:arXiv.org:hep-th:0208183');
31             my $xml = $x->get_record('oai:arXiv.org:hep-th:0208183');
32              
33             =head1 DESCRIPTION
34              
35             This modules creates an index on XMLtapes to enable fast retrieval of XML documents
36             from the archive. The index files are stored next to the XMLtape.
37              
38             =cut
39             package XML::Tape::Index;
40 1     1   1667 use strict;
  1         2  
  1         32  
41 1     1   593 use DB_File;
  0            
  0            
42             use XML::Tape;
43             use Digest::MD5 qw(md5);
44             require Exporter;
45             use vars qw($VERSION);
46              
47             ( $VERSION ) = '$Revision: 1.5 $ ' =~ /\$Revision:\s+([^\s]+)/;;
48              
49             @XML::Tape::Index::ISA = qw(Exporter);
50             @XML::Tape::Index::EXPORT_OK = qw(indexopen indexexists indexdrop);
51             %XML::Tape::Index::EXPORT_TAGS = (all => [qw(indexopen indexexists indexdrop)]);
52             $XML::Tape::Index::VERBOSE = 0;
53             $XML::Tape::Index::CACHE_SIZE = 4 * 1024 * 1024;
54              
55             sub _get_index {
56             my ($filename) = @_;
57             return {
58             adm_index_file => "$filename.adm" ,
59             rec_index_file => "$filename.rec" ,
60             dat_index_file => "$filename.dat" ,
61             }
62             }
63              
64             =head1 METHODS
65              
66             =over 4
67              
68             =item $x = indexopen($tape_file, $flag)
69              
70             This function opens an index for reading or writing. The parameter tape_file
71             is the location of a XMLtape archive. The flag is "w" when creating a new index or
72             "r" when reading an index. An XML::Tape::Index instance will be returned on
73             success or undef on failure.
74              
75             =cut
76             sub indexopen {
77             my ($tape_file, $flag, $mode) = @_;
78             my (%admh,$admh);
79             my (%rech,$rech);
80             my (%idsh,$idsh);
81             my (%dath,$dath);
82             $mode = 0644 unless $mode;
83              
84             my $files = &_get_index($tape_file);
85              
86             my $this = bless {} , 'XML::Tape::Index';
87             $this->{mode} = $flag;
88              
89             if ($flag eq 'w') {
90             $flag = O_CREAT | O_RDWR;
91             }
92             elsif ($flag eq 'r') {
93             $flag = O_RDONLY;
94             }
95             else {
96             die "usage: indexopen(\$tape_file, 'r' | 'w')";
97             }
98              
99             my $f_hash = new DB_File::HASHINFO;
100             $f_hash->{cachesize} = $XML::Tape::Index::CACHE_SIZE;
101             my $f_btree = new DB_File::BTREEINFO;
102             $f_btree->{cachesize} = $XML::Tape::Index::CACHE_SIZE;
103             $f_btree->{flags} = R_DUP;
104              
105             $admh = tie %admh, 'DB_File' , $files->{adm_index_file} , $flag, $mode, $f_hash
106             || die "can't tie " . $files->{adm_index_file} . ": $!";
107             $rech = tie %rech, 'DB_File' , $files->{rec_index_file} , $flag, $mode, $f_hash
108             || die "can't tie " . $files->{rec_index_file} . ": $!";
109             $dath = tie %dath, 'DB_File' , $files->{dat_index_file} , $flag, $mode, $f_btree
110             || die "can't tie " . $files->{dat_index_file} . ": $!";
111              
112             $this->{tape_file} = $tape_file;
113             $this->{admh} = $admh;
114             $this->{rech} = $rech;
115             $this->{dath} = $dath;
116             $this->{t_admh} = \%admh;
117             $this->{t_rech} = \%rech;
118             $this->{t_dath} = \%dath;
119             return $this;
120             }
121              
122             =item $x->reindex()
123              
124             This method reads the XMLtape extracts all identifier and datestamps from
125             it and stores the byte positions of all records in the index.
126              
127             =cut
128             sub reindex {
129             my ($this) = @_;
130              
131             die "reindex: only allowed in 'w' mode" unless ($this->{mode} eq 'w');
132              
133             my $num_of_rec = 0;
134             my $tape = XML::Tape::tapeopen($this->{tape_file}, 'r') || return undef;
135              
136             my $_start = time();
137             my $earliest_datestamp = undef;
138             while (my $record = $tape->get_record()) {
139             $num_of_rec++;
140             my $id = $record->getIdentifier();
141             my $date = $record->getDate();
142             my $start = $record->getStartByte();
143             my $length = $record->getEndByte() - $start;
144             my $value = join("\t", $id, $date, $start, $length);
145             my $key = md5($id);
146             $this->{rech}->put($key,$value);
147             $this->{dath}->put($date,$key);
148              
149             if ($XML::Tape::Index::VERBOSE && $num_of_rec % 10000 == 0) {
150             my $speed = int($num_of_rec/(time - $_start + 1));
151             print "record: $num_of_rec ($speed r/s) read: " . $record->getEndByte() . " bytes\n";
152             }
153              
154             my $comp_date = $date; $comp_date =~ s/\D+//g;
155             if ( ! defined $earliest_datestamp || $earliest_datestamp->{val} > $comp_date ) {
156             $earliest_datestamp->{val} = $comp_date;
157             $earliest_datestamp->{str} = $date;
158             }
159             }
160             $tape->tapeclose();
161              
162             $this->{admh}->put('tapefile', $this->{tape_file});
163             $this->{admh}->put('recnum', $num_of_rec);
164             $this->{admh}->put('earliest', $earliest_datestamp->{str});
165              
166             return $num_of_rec;
167             }
168              
169             =item $x->list_identifiers([$token])
170              
171             =item $x->list_identifiers($from,$until)
172              
173             Use this method to iterate through the index to return all records. This method
174             returns an index record on success or undef when no more records are available.
175             Each index record is a HASH reference containing the fields 'identifier', 'date',
176             'start' (the starting byte of the XML document in the XMLtape), 'len' (the length of
177             the XML document in the XMLtape) and 'token'. The 'token' field should be used to
178             return the next index record. One can filter the returned indexed records by
179             using two arguments at the first list_identifiers method invocation. Only
180             index records with dates greater or equal than 'from' and less than 'until'
181             will be returned by subsequent list_identifier requests. E.g.
182              
183             # Return all index records...
184             for (my $r = $x->list_identifiers();
185             defined($r);
186             $r = $x->list_identifiers($r->{token}) {
187             }
188              
189             # Return all index records with dates between 2000-01-01 and 2005-12-31...
190             for (my $r = $x->list_identifiers(
191             '2001-01-01T00:00:00Z',
192             '2005-12-31T23:59:59Z'
193             );
194             defined($r);
195             $r = $x->list_identifiers($r->{token}) {
196             }
197              
198             =cut
199             sub list_identifiers {
200             my ($this) = shift;
201             my ($from,$until,$md5);
202            
203             die "list_identifiers: only allowed in 'r' mode" unless ($this->{mode} eq 'r');
204              
205             # If we have two arguments we need to filter on 'from' and 'until' date...
206             if (@_ == 2) {
207             ($from,$until) = @_;
208             $this->{'from'} = $from;
209             $this->{'until'} = $until;
210             }
211             # If we have one argument than it is a resumption token...
212             elsif (@_ == 1) {
213             ($from,$md5) = split(/,/,shift,2);
214             $md5 = pack("H*",$md5);
215             $until = $this->{'until'};
216             }
217             # Else, we need to return all entries..
218             else {
219             $from = $until = undef;
220             $this->{'from'} = $from;
221             $this->{'until'} = $until;
222             }
223              
224             my $status;
225              
226             if ($md5) {
227             $status = $this->{dath}->find_dup($from, $md5);
228             $status = $this->{dath}->seq($from, $md5, R_NEXT) if ($status == 0);
229             }
230             elsif ($from) {
231             $status = $this->{dath}->seq($from, $md5, R_CURSOR);
232             }
233             else {
234             $status = $this->{dath}->seq($from, $md5, R_FIRST);
235             }
236              
237             return undef unless ($status == 0);
238             return undef if (defined $until && ($from cmp $until) >= 0);
239              
240             my $values;
241             $status = $this->{rech}->get($md5,$values);
242              
243             return undef unless ($status == 0);
244              
245             my (@field) = split(/\t/,$values);
246             return {
247             'identifier' => $field[0] ,
248             'date' => $field[1] ,
249             'start' => $field[2] ,
250             'length' => $field[3] ,
251             'token' => $field[1] . "," . unpack("H*",$md5)
252             };
253             }
254              
255             =item $x->get_earlist_date()
256              
257             This methods returns earliest date in the index file
258              
259             =cut
260             sub get_earliest_date {
261             my ($this, $id) = @_;
262             my $values;
263             $this->{admh}->get('earliest',$values);
264             return $values;
265             }
266              
267             =item $x->get_tape_file()
268              
269             This methods returns name of the tape file associated with this index.
270              
271             =cut
272             sub get_tape_file {
273             my ($this, $id) = @_;
274             my $values;
275             $this->{admh}->get('tapefile',$values);
276             return $values;
277             }
278              
279             =item $x->get_num_of_records()
280              
281             This methods returns the number of record in an index.
282              
283             =cut
284             sub get_num_of_records {
285             my ($this, $id) = @_;
286             my $values;
287             $this->{admh}->get('recnum',$values);
288             return $values;
289             }
290              
291             =item $x->get_identifier($identifier)
292              
293             This method returns an index record given an identifier as argument. When
294             no matching index record can be found undef will be returned. The index
295             record is a HASH reference containing the fields 'identifier', 'date',
296             'start' and 'len' (see above).
297              
298             =cut
299             sub get_identifier {
300             my ($this, $id) = @_;
301             my $md5 = md5($id);
302             my $values;
303              
304             die "get_identifier: only allowed in 'r' mode" unless ($this->{mode} eq 'r');
305              
306             $this->{rech}->get($md5,$values);
307              
308             return undef unless $values;
309              
310             my (@field) = split(/\t/,$values);
311             return {
312             'identifier' => $field[0] ,
313             'date' => $field[1] ,
314             'start' => $field[2] ,
315             'len' => $field[3] ,
316             };
317             }
318              
319             =item $x->get_record($identifier)
320              
321             This method returns an XML document from the XMLtape given an identifier as
322             argument. When no matching record can be found undef will be returned.
323              
324             =cut
325             sub get_record {
326             my ($this, $id) = @_;
327              
328             die "get_record: only allowed in 'r' mode" unless ($this->{mode} eq 'r');
329              
330             local(*F);
331             my $rec = $this->get_identifier($id);
332            
333             return undef unless $rec;
334              
335             my $xml;
336             if ($rec->{start} && $rec->{len}) {
337             open(F, $this->{tape_file}) || return undef;
338             seek(F, $rec->{start}, 0);
339             read(F, $xml, $rec->{len});
340             close(F);
341             }
342             return $xml;
343             }
344              
345             =item $x->indexclose();
346              
347             Closes the XMLtape index.
348              
349             =cut
350             sub indexclose {
351             my ($this) = @_;
352              
353             $this->{admh} = undef;
354             $this->{rech} = undef;
355             $this->{dath} = undef;
356             untie %{$this->{t_admh}};
357             untie %{$this->{t_rech}};
358             untie %{$this->{t_dath}};
359             }
360              
361             =item indexexists($tape_file)
362              
363             This class method returns true when an index on the XMLtape with location
364             $tape_file exists, returns false otherwise.
365              
366             =cut
367             sub indexexists {
368             my ($filename) = @_;
369            
370             my $files = &_get_index($filename);
371              
372             return (-e $files->{adm_index_file} && -e $files->{rec_index_file} && -e $files->{dat_index_file});
373             }
374              
375             =item indexdrop($tape_file)
376              
377             This class method deletes the index associated with the XMLtape with location
378             $tape_file.
379              
380             =cut
381             sub indexdrop {
382             my ($filename) = @_;
383              
384             my $files = &_get_index($filename);
385              
386             unlink $files->{adm_index_file};
387             unlink $files->{rec_index_file};
388             unlink $files->{dat_index_file};
389             }
390              
391             =head1 BUGS
392              
393             The XML::Tape::Index doesn't lock XMLtape before writing. It is possible to
394             overwrite and index while another process is reading it.
395              
396             =head1 CREDITS
397              
398             XMLtape archives were developed by the Digital Library Research & Prototyping
399             team at Los Alamos National Laboratory.
400              
401             =head1 SEE ALSO
402              
403             L
404              
405             =head1 AUTHOR
406              
407             Patrick Hochstenbach
408              
409             =cut
410             1;