source: main/trunk/greenstone2/perllib/plugins/LOMPlugin.pm@ 24548

Last change on this file since 24548 was 24548, checked in by ak19, 13 years ago

Part 2 of previous commit (r24547). Added new abstract plugin MetadataRead? that defines can_process_this_file_for_metadata that MetadataPlugin? subclasses can inherit (if MetadataRead? is listed first in the ISA inheritance list) and which will then override the one defined in BasePlugin?. For now committing MARC, ISIS and OAIPlugins which now additionally inherit from MetadataRead?. Other metadataPlugins also need to be committed.

  • Property svn:keywords set to Author Date Id Revision
File size: 19.2 KB
Line 
1###########################################################################
2#
3# LOMPlugin.pm -- plugin for import the collection from LOM
4#
5# A component of the Greenstone digital library software
6# from the New Zealand Digital Library Project at the
7# University of Waikato, New Zealand.
8#
9# Copyright (C) 2005 New Zealand Digital Library Project
10#
11# This program is free software; you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation; either version 2 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program; if not, write to the Free Software
23# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
24#
25###########################################################################
26
27### Note this plugin currently can't download source documents from outside if you are behind a firewall.
28# Unless, you set the http_proxy environment variable to be your proxy server,
29# and set proxy_user and proxy_password in .wgetrc file in home directory.
30# (does that work on windows??)
31
32package LOMPlugin;
33
34use ReadTextFile;
35use MetadataPass;
36use MetadataRead;
37use XMLParser;
38use Cwd;
39
40# methods with identical signatures take precedence in the order given in the ISA list.
41sub BEGIN {
42 @ISA = ('MetadataRead', 'ReadTextFile', 'MetadataPass');
43}
44
45use strict; # every perl program should have this!
46no strict 'refs'; # make an exception so we can use variables as filehandles
47
48
49my $arguments =
50 [ { 'name' => "process_exp",
51 'desc' => "{BasePlugin.process_exp}",
52 'type' => "string",
53 'deft' => &get_default_process_exp(),
54 'reqd' => "no" },
55 { 'name' => "root_tag",
56 'desc' => "{LOMPlugin.root_tag}",
57 'type' => "regexp",
58 'deft' => q/^(?i)lom$/,
59 'reqd' => "no" },
60 { 'name' => "check_timestamp",
61 'desc' => "{LOMPlugin.check_timestamp}",
62 'type' => "flag" },
63 { 'name' => "download_srcdocs",
64 'desc' => "{LOMPlugin.download_srcdocs}",
65 'type' => "regexp",
66 'deft' => "",
67 'reqd' => "no" }];
68
69my $options = { 'name' => "LOMPlugin",
70 'desc' => "{LOMPlugin.desc}",
71 'abstract' => "no",
72 'inherits' => "yes",
73 'args' => $arguments };
74
75
76
77my ($self);
78sub new {
79 my $class = shift (@_);
80 my ($pluginlist,$inputargs,$hashArgOptLists) = @_;
81 push(@$pluginlist, $class);
82
83 push(@{$hashArgOptLists->{"ArgList"}},@{$arguments});
84 push(@{$hashArgOptLists->{"OptList"}},$options);
85
86 $self = new ReadTextFile($pluginlist, $inputargs, $hashArgOptLists);
87
88 if ($self->{'info_only'}) {
89 # don't worry about creating the XML parser as all we want is the
90 # list of plugin options
91 return bless $self, $class;
92 }
93
94 #create XML::Parser object for parsing dublin_core.xml files
95 my $parser = new XML::Parser('Style' => 'Stream',
96 'Handlers' => {'Char' => \&Char,
97 'Doctype' => \&Doctype
98 });
99 $self->{'parser'} = $parser;
100
101 $self->{'extra_blocks'} = {};
102
103 return bless $self, $class;
104}
105
106sub get_default_process_exp {
107 my $self = shift (@_);
108
109 return q^(?i)\.xml$^;
110}
111
112
113sub can_process_this_file {
114 my $self = shift(@_);
115 my ($filename) = @_;
116
117 if ($self->SUPER::can_process_this_file($filename) && $self->check_doctype($filename)) {
118 return 1; # its a file for us
119 }
120 return 0;
121}
122
123sub metadata_read {
124 my $self = shift (@_);
125 my ($pluginfo, $base_dir, $file, $block_hash,
126 $extrametakeys, $extrametadata, $extrametafile,
127 $processor, $gli, $aux) = @_;
128
129 my $outhandle = $self->{'outhandle'};
130
131 # can we process this file??
132 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file);
133 return undef unless $self->can_process_this_file_for_metadata($filename_full_path);
134
135 $file =~ s/^[\/\\]+//; # $file often begins with / so we'll tidy it up
136
137 print $outhandle "LOMPlugin: extracting metadata from $file\n"
138 if $self->{'verbosity'} > 1;
139
140 my ($dir,$tail) = $filename_full_path =~ /^(.*?)([^\/\\]*)$/;
141 $self->{'output_dir'} = $dir;
142
143 eval {
144 $self->{'parser'}->parsefile($filename_full_path);
145 };
146
147 if ($@) {
148 print $outhandle "LOMPlugin: skipping $filename_full_path as not conformant to LOM syntax\n" if ($self->{'verbosity'} > 1);
149 print $outhandle "\n Perl Error:\n $@\n" if ($self->{'verbosity'}>2);
150 return 0;
151 }
152
153 $self->{'output_dir'} = undef;
154
155 my $file_re;
156 my $lom_srcdoc = $self->{'lom_srcdoc'};
157
158 if (defined $lom_srcdoc) {
159 my $dirsep = &util::get_re_dirsep();
160 $lom_srcdoc =~ s/^$base_dir($dirsep)//;
161 $self->{'extra_blocks'}->{$file}++;
162 $file_re = $lom_srcdoc;
163 }
164 else {
165 $file_re = $tail;
166 }
167 $file_re = &util::filename_to_regex($file_re);
168 $self->{'lom_srcdoc'} = undef; # reset for next file to be processed
169
170 push(@$extrametakeys,$file_re);
171 $extrametadata->{$file_re} = $self->{'saved_metadata'};
172 if (defined $lom_srcdoc) {
173 # copied from oaiplugin
174 if (!defined $extrametafile->{$file_re}) {
175 $extrametafile->{$file_re} = {};
176 }
177 #maps the file to full path
178 $extrametafile->{$file_re}->{$file} = $filename_full_path;
179 }
180
181 return 1;
182}
183
184sub check_doctype {
185 $self = shift (@_);
186
187 my ($filename) = @_;
188
189 if (open(XMLIN,"<$filename")) {
190 my $doctype = $self->{'root_tag'};
191 ## check whether the doctype has the same name as the root element tag
192 while (defined (my $line = <XMLIN>)) {
193 ## find the root element
194 if ($line =~ /<([\w\d:]+)[\s>]/){
195 my $root = $1;
196 if ($root !~ $doctype){
197 close(XMLIN);
198 return 0;
199 }
200 else {
201 close(XMLIN);
202 return 1;
203 }
204 }
205 }
206 close(XMLIN);
207 }
208
209 return undef; # haven't found a valid line
210
211}
212
213sub read_file {
214 my $self = shift (@_);
215 my ($filename, $encoding, $language, $textref) = @_;
216
217 my $metadata_table = $self->{'metadata_table'};
218
219 my $rawtext = $metadata_table->{'rawtext'};
220
221 delete $metadata_table->{'rawtext'};
222
223 $$textref = $rawtext;
224}
225
226sub read {
227 my $self = shift (@_);
228 my ($pluginfo, $base_dir, $file, $block_hash, $metadata, $processor, $maxdocs, $total_count, $gli) = @_;
229
230 my $outhandle = $self->{'outhandle'};
231
232 return 0 if (defined $self->{'extra_blocks'}->{$file});
233
234 # can we process this file??
235 my ($filename_full_path, $filename_no_path) = &util::get_full_filenames($base_dir, $file);
236 return undef unless $self->can_process_this_file($filename_full_path);
237
238 $self->{'metadata_table'} = $metadata;
239
240 my $lom_language = $metadata->{'lom_language'};
241
242 my $store_input_encoding;
243 my $store_extract_language;
244 my $store_default_language;
245 my $store_default_encoding;
246
247 if (defined $lom_language) {
248 delete $metadata->{'lom_language'};
249
250 $store_input_encoding = $self->{'input_encoding'};
251 $store_extract_language = $self->{'extract_language'};
252 $store_default_language = $self->{'default_language'};
253 $store_default_encoding = $self->{'default_encoding'};
254
255 $self->{'input_encoding'} = "utf8";
256 $self->{'extract_language'} = 0;
257 $self->{'default_language'} = $lom_language;
258 $self->{'default_encoding'} = "utf8";
259 }
260
261 my $rv = $self->SUPER::read(@_);
262
263 if (defined $lom_language) {
264 $self->{'input_encoding'} = $store_input_encoding;
265 $self->{'extract_language'} = $store_extract_language;
266 $self->{'default_language'} = $store_default_language;
267 $self->{'default_encoding'} = $store_default_encoding;
268 }
269
270 $self->{'metadata_table'} = undef;
271
272 return $rv;
273}
274
275# do plugin specific processing of doc_obj
276sub process {
277 my $self = shift (@_);
278 my ($textref, $pluginfo, $base_dir, $file, $metadata, $doc_obj, $gli) = @_;
279 my $outhandle = $self->{'outhandle'};
280
281 my $cursection = $doc_obj->get_top_section();
282 $doc_obj->add_utf8_text($cursection, $$textref);
283
284 return 1;
285}
286
287sub Doctype {
288 my ($expat, $name, $sysid, $pubid, $internal) = @_;
289
290 my $root_tag = $self->{'root_tag'};
291
292 if ($name !~ /$root_tag/) {
293 die "Root tag $name does not match regular expression $root_tag";
294 }
295}
296
297sub StartTag {
298 my ($expat, $element) = @_;
299
300 my %attr = %_;
301
302 my $raw_tag = "&lt;$element";
303 map { $raw_tag .= " $_=\"$attr{$_}\""; } keys %attr;
304 $raw_tag .= "&gt;";
305
306 if ($element =~ m/$self->{'root_tag'}/) {
307 $self->{'raw_text'} = $raw_tag;
308
309 $self->{'saved_metadata'} = {};
310 $self->{'metaname_stack'} = [];
311 $self->{'lom_datatype'} = "";
312 $self->{'lom_language'} = undef;
313 $self->{'metadatatext'} = "<table class=\"metadata\" width=\"_pagewidth_\" >\n";
314 }
315 else {
316 my $xml_depth = scalar(@{$self->{'metaname_stack'}});
317 $self->{'raw_text'} .= "\n";
318 $self->{'raw_text'} .= "&nbsp;&nbsp;" x $xml_depth;
319 $self->{'raw_text'} .= $raw_tag;
320
321 my $metaname_stack = $self->{'metaname_stack'};
322 push(@$metaname_stack,$element);
323 if (scalar(@$metaname_stack)==1) {
324 # top level LOM category
325 my $style = "class=\"metadata\"";
326 my $open_close
327 = "<a id=\"${element}opencloselink\" href=\"javascript:hideTBodyArea('$element')\">\n";
328 $open_close
329 .= "<img id=\"${element}openclose\" border=\"0\" src=\"_httpopenmdicon_\"></a>\n";
330
331 my $header_line = " <tr $style ><th $style colspan=\"3\">$open_close \u$element</th></tr>\n";
332 my $md_tbody = "<tbody id=\"$element\">\n";
333
334 $self->{'mdheader'} = $header_line;
335 $self->{'mdtbody'} = $md_tbody;
336 $self->{'mdtbody_text'} = "";
337 }
338 }
339}
340
341sub EndTag {
342 my ($expat, $element) = @_;
343
344 my $raw_tag = "&lt;/$element&gt;";
345
346 if ($element =~ m/$self->{'root_tag'}/) {
347 $self->{'raw_text'} .= $raw_tag;
348
349 my $metadatatext = $self->{'metadatatext'};
350 $metadatatext .= "</table>";
351
352 my $raw_text = $self->{'raw_text'};
353
354 $self->{'saved_metadata'}->{'MetadataTable'} = $metadatatext;
355 $self->{'metadatatext'} = "";
356
357 $self->{'saved_metadata'}->{'rawtext'} = $raw_text;
358 $self->{'raw_text'} = "";
359
360 if (defined $self->{'lom_language'}) {
361 $self->{'saved_metadata'}->{'lom_language'} = $self->{'lom_language'};
362 $self->{'lom_language'} = undef;
363 }
364 }
365 else {
366 my $metaname_stack = $self->{'metaname_stack'};
367
368 if (scalar(@$metaname_stack)==1) {
369 my $header_line = $self->{'mdheader'};
370 my $tbody_start = $self->{'mdtbody'};
371 my $tbody_text = $self->{'mdtbody_text'};
372 if ($tbody_text !~ m/^\s*$/s) {
373 my $tbody_end = "</tbody>\n";
374 my $table_chunk
375 = $header_line.$tbody_start.$tbody_text.$tbody_end;
376
377 $self->{'metadatatext'} .= $table_chunk;
378 }
379 $self->{'mdtheader'} = "";
380 $self->{'mdtbody'} = "";
381 $self->{'mdtbody_text'} = "";
382 }
383
384 pop(@$metaname_stack);
385
386 my $xml_depth = scalar(@{$self->{'metaname_stack'}});
387 $self->{'raw_text'} .= "\n";
388 $self->{'raw_text'} .= "&nbsp;&nbsp;" x $xml_depth;
389 $self->{'raw_text'} .= $raw_tag;
390 }
391}
392
393sub process_datatype_info
394{
395 my $self = shift(@_);
396 my ($metaname_stack,$md_content) = @_;
397
398 my @without_dt_stack = @$metaname_stack; # without datatype stack
399
400 my $innermost_element = $without_dt_stack[$#without_dt_stack];
401
402 # Loose last item if encoding datatype information
403 if ($innermost_element =~ m/^(lang)?string$/) {
404 $self->{'lom_datatype'} = $innermost_element;
405
406 pop @without_dt_stack;
407 $innermost_element = $without_dt_stack[$#without_dt_stack];
408 }
409 elsif ($innermost_element =~ m/^date(Time)?$/i) {
410 if ($innermost_element =~ m/^date$/i) {
411 $self->{'lom_datatype'} = "dateTime";
412 }
413 else {
414 $self->{'lom_datatype'} = $innermost_element;
415
416 pop @without_dt_stack;
417 $innermost_element = $without_dt_stack[$#without_dt_stack];
418 }
419
420 if ($md_content =~ m/^(\d{1,2})\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*\s*(\d{4})/i) {
421 my ($day,$mon,$year) = ($1,$2,$3);
422
423 my %month_lookup = ( 'jan' => 1, 'feb' => 2, 'mar' => 3,
424 'apr' => 4, 'may' => 5, 'jun' => 6,
425 'jul' => 7, 'aug' => 8, 'sep' => 9,
426 'oct' => 10, 'nov' => 11, 'dec' => 12 );
427
428 my $mon_num = $month_lookup{lc($mon)};
429
430 $md_content = sprintf("%d%02d%02d",$year,$mon_num,$day);
431 }
432
433 $md_content =~ s/\-//g;
434 }
435
436 if ($innermost_element eq "source") {
437 $self->{'lom_source'} = $md_content;
438 }
439 elsif ($innermost_element eq "value") {
440 $self->{'lom_value'} = $md_content;
441 }
442
443 return (\@without_dt_stack,$innermost_element,$md_content);
444}
445
446sub reset_datatype_info
447{
448 my $self = shift(@_);
449
450 $self->{'lom_datatype'} = "";
451}
452
453
454sub pretty_print_text
455{
456 my $self = shift(@_);
457
458 my ($pretty_print_text) = @_;
459
460## $metavalue_utf8 = &util::hyperlink_text($metavalue_utf8);
461 $pretty_print_text = &util::hyperlink_text($pretty_print_text);
462
463#### $pretty_print_text =~ s/(BEGIN:vCard.*END:vCard)/<pre>$1<\/pre>/sg;
464
465 if ($self->{'lom_datatype'} eq "dateTime") {
466 if ($pretty_print_text =~ m/^(\d{4})(\d{2})(\d{2})$/) {
467 $pretty_print_text = "$1-$2-$3";
468 }
469 }
470
471 return $pretty_print_text;
472}
473
474sub pretty_print_table_tr
475{
476 my $self = shift (@_);
477 my ($without_dt_stack) = @_;
478
479 my $style = "class=\"metadata\"";
480
481 my $innermost_element = $without_dt_stack->[scalar(@$without_dt_stack)-1];
482 my $outermost_element = $without_dt_stack->[0];
483
484 # Loose top level stack item (already named in pretty print table)
485 my @pretty_print_stack = @$without_dt_stack;
486 shift @pretty_print_stack;
487
488 if ($innermost_element eq "source") {
489 return if (!defined $self->{'lom_value'});
490 }
491
492 if ($innermost_element eq "value") {
493 return if (!defined $self->{'lom_source'});
494 }
495
496 my $pretty_print_text = "";
497
498 if (($innermost_element eq "value") || ($innermost_element eq "source")) {
499 my $source = $self->{'lom_source'};
500 my $value = $self->pretty_print_text($self->{'lom_value'});
501
502 $self->{'lom_source'} = undef;
503 $self->{'lom_value'} = undef;
504
505 pop @pretty_print_stack;
506
507 $pretty_print_text = "<td $style>$source</td><td $style>$value</td>";
508 }
509 else {
510 $pretty_print_text = $self->pretty_print_text($_);
511 $pretty_print_text = "<td $style colspan=2>$pretty_print_text</td>";
512 }
513 my $pretty_print_fmn = join(' : ',map { "\u$_"; } @pretty_print_stack);
514
515
516 # my $tr_attr = "id=\"$outermost_element\" style=\"display:block;\"";
517 my $tr_attr = "$style id=\"$outermost_element\"";
518
519 my $mdtext_line = " <tr $tr_attr><td $style><nobr>$pretty_print_fmn</nobr></td>$pretty_print_text</tr>\n";
520 $self->{'mdtbody_text'} .= $mdtext_line;
521}
522
523
524sub check_for_language
525{
526 my $self = shift(@_);
527 my ($innermost_element,$md_content) = @_;
528
529 # Look for 'language' tag
530 if ($innermost_element eq "language") {
531 my $lom_lang = $self->{'lom_language'};
532
533 if (defined $lom_lang) {
534 my $new_lom_lang = $md_content;
535 $new_lom_lang =~ s/-.*//; # remove endings like -US or -GB
536
537 if ($lom_lang ne $new_lom_lang) {
538 my $outhandle = $self->{'outhandle'};
539
540 print $outhandle "Warning: Conflicting general language in record\n";
541 print $outhandle " $new_lom_lang (previous value for language = $lom_lang)\n";
542 }
543 # otherwise, existing value OK => do nothing
544 }
545 else {
546 $lom_lang = $md_content;
547 $lom_lang =~ s/-.*//; # remove endings like -US or -GB
548
549 $self->{'lom_language'} = $lom_lang;
550 }
551 }
552}
553
554sub found_specific_identifier
555{
556 my $self = shift(@_);
557 my ($specific_id,$full_mname,$md_content) = @_;
558
559 my $found_id = 0;
560 if ($full_mname eq $specific_id) {
561 if ($md_content =~ m/^(http|ftp):/) {
562 $found_id = 1;
563 }
564 }
565
566 return $found_id;
567}
568
569sub download_srcdoc
570{
571 my $self = shift(@_);
572 my ($doc_url) = @_;
573
574 my $outhandle = $self->{'outhandle'};
575 my $output_dir = $self->{'output_dir'};
576
577 $output_dir = &util::filename_cat($output_dir,"_gsdldown.all");
578
579 if (! -d $output_dir) {
580 mkdir $output_dir;
581 }
582
583 my $re_dirsep = &util::get_re_dirsep();
584 my $os_dirsep = &util::get_dirsep();
585
586 my $file_url = $doc_url;
587 $file_url =~ s/$re_dirsep/$os_dirsep/g;
588 $file_url =~ s/^(http|ftp):\/\///;
589 $file_url .= "index.html" if ($file_url =~ m/\/$/);
590
591 my $full_file_url = &util::filename_cat($output_dir,$file_url);
592 # the path to srcdoc will be used later in extrametadata to associate
593 # the lom metadata with the document. Needs to be relative to current
594 # directory.
595 my $srcdoc_path = &util::filename_cat("_gsdldown.all", $file_url);
596 my $check_timestamp = $self->{'check_timestamp'};
597 my $status;
598
599 if (($check_timestamp) || (!$check_timestamp && !-e $full_file_url)) {
600 if (!-e $full_file_url) {
601 print $outhandle "Mirroring $doc_url\n";
602 }
603 else {
604 print $outhandle "Checking to see if update needed for $doc_url\n";
605 }
606
607 # on linux, if we pass an absolute path as -P arg to wget, then it
608 # stuffs up the
609 # URL rewriting in the file. Need a relative path or none, so now
610 # we change working directory first.
611 my $changed_dir = 0;
612 my $current_dir = cwd();
613 my $wget_cmd = "";
614 if ($ENV{'GSDLOS'} ne "windows") {
615 $changed_dir = 1;
616
617 chdir "$output_dir";
618 $wget_cmd = "wget -nv --timestamping -k -p \"$doc_url\"";
619 } else {
620 $wget_cmd = "wget -nv -P \"$output_dir\" --timestamping -k -p \"$doc_url\"";
621 }
622 ##print STDERR "**** wget = $wget_cmd\n";
623
624
625 $status = system($wget_cmd);
626 if ($changed_dir) {
627 chdir $current_dir;
628 }
629 if ($status==0) {
630 $self->{'lom_srcdoc'} = $srcdoc_path;
631 }
632 else {
633 $self->{'lom_srcdoc'} = undef;
634 print $outhandle "Error: failed to execute $wget_cmd\n";
635 }
636 }
637 else {
638 # not time-stamping and file already exists
639 $status=0;
640 $self->{'lom_srcdoc'} = $srcdoc_path;
641 }
642
643 return $status==0;
644
645}
646
647
648sub check_for_identifier
649{
650 my $self = shift(@_);
651 my ($full_mname,$md_content) = @_;
652
653 my $success = 0;
654
655 my $download_re = $self->{'download_srcdocs'};
656 if (($download_re ne "") && $md_content =~ m/$download_re/) {
657
658 if ($self->found_specific_identifier("general^identifier^entry",$full_mname,$md_content)) {
659 $success = $self->download_srcdoc($md_content);
660 }
661
662 if (!$success) {
663 if ($self->found_specific_identifier("technical^location",$full_mname,$md_content)) {
664 $success = $self->download_srcdoc($md_content);
665 }
666 }
667 }
668
669 return $success;
670}
671
672
673sub Text {
674 if ($_ !~ m/^\s*$/) {
675 #
676 # Work out indentations and line wraps for raw XML
677 #
678 my $xml_depth = scalar(@{$self->{'metaname_stack'}})+1;
679 my $indent = "&nbsp;&nbsp;" x $xml_depth;
680
681 my $formatted_text = "\n".$_;
682
683 # break into lines < 80 chars on space
684 $formatted_text =~ s/(.{50,80})\s+/$1\n/mg;
685 $formatted_text =~ s/^/$indent/mg;
686 ## $formatted_text =~ s/\s+$//s;
687
688 $self->{'raw_text'} .= $formatted_text;
689 }
690
691 my $metaname_stack = $self->{'metaname_stack'};
692 if (($_ !~ /^\s*$/) && (scalar(@$metaname_stack)>0)) {
693
694 my ($without_dt_stack,$innermost_element,$md_content)
695 = $self->process_datatype_info($metaname_stack,$_);
696
697 $self->pretty_print_table_tr($without_dt_stack);
698
699 my $full_mname = join('^',@{$without_dt_stack});
700 $self->set_filere_metadata(lc($full_mname),$md_content);
701
702 $self->check_for_language($innermost_element,$md_content);
703 $self->check_for_identifier($full_mname,$md_content); # source doc
704
705 $self->reset_datatype_info();
706 }
707}
708
709# This Char function overrides the one in XML::Parser::Stream to overcome a
710# problem where $expat->{Text} is treated as the return value, slowing
711# things down significantly in some cases.
712sub Char {
713 $_[0]->{'Text'} .= $_[1];
714 return undef;
715}
716
7171;
Note: See TracBrowser for help on using the repository browser.