File Coverage

lib/Mobile/Wurfl.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Mobile::Wurfl;
2              
3             $VERSION = '1.09';
4              
5 1     1   880 use strict;
  1         2  
  1         50  
6 1     1   7 use warnings;
  1         2  
  1         38  
7 1     1   18 use DBI;
  1         2  
  1         57  
8 1     1   672 use DBD::mysql;
  0            
  0            
9             use XML::Parser;
10             require LWP::UserAgent;
11             use HTTP::Date;
12             use Template;
13             use File::Spec;
14             use File::Basename;
15             use IO::Uncompress::Unzip qw(unzip $UnzipError);;
16             use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
17              
18             my %tables = (
19             device => [ qw( id actual_device_root user_agent fall_back ) ],
20             capability => [ qw( groupid name value deviceid ) ],
21             );
22              
23             sub new
24             {
25             my $class = shift;
26             my %opts = (
27             wurfl_home => "/tmp",
28             db_descriptor => "DBI:mysql:database=wurfl:host=localhost",
29             db_username => 'wurfl',
30             db_password => 'wurfl',
31             device_table_name => 'device',
32             capability_table_name => 'capability',
33             wurfl_url => q{http://sourceforge.net/projects/wurfl/files/WURFL/latest/wurfl-latest.xml.gz/download},
34             verbose => 0,
35             @_
36             );
37              
38             my $self = bless \%opts, $class;
39             if ( ! $self->{verbose} )
40             {
41             open( STDERR, ">" . File::Spec->devnull() )
42             }
43             elsif ( $self->{verbose} == 1 )
44             {
45             open( STDERR, ">$self->{wurfl_home}/wurfl.log" );
46             }
47             else
48             {
49             warn "log to STDERR\n";
50             }
51             print STDERR "connecting to $self->{db_descriptor} as $self->{db_username}\n";
52             $self->{dbh} ||= DBI->connect(
53             $self->{db_descriptor},
54             $self->{db_username},
55             $self->{db_password},
56             { RaiseError => 1 }
57             ) or die "Cannot connect to $self->{db_descriptor}: " . $DBI::errstr;
58             die "no wurfl_url\n" unless $self->{wurfl_url};
59              
60             #get a filename from the URL and remove .zip or .gzip suffix
61             my $name = (fileparse($self->{wurfl_url}, '.zip', '.gzip'))[0];
62             $self->{wurfl_file} = "$self->{wurfl_home}/$name";
63              
64             $self->{ua} = LWP::UserAgent->new;
65             return $self;
66             }
67              
68              
69             sub _tables_exist
70             {
71             my $self = shift;
72             my %db_tables = map { my $key = $_ =~ /(.*)\.(.*)/ ? $2 : $_ ; $key => 1 } $self->{dbh}->tables();
73             for my $table ( keys %tables )
74             {
75             return 0 unless $db_tables{$self->{dbh}->quote_identifier($table)};
76              
77             }
78             return 1;
79             }
80              
81             sub _init
82             {
83             my $self = shift;
84             return if $self->{initialised};
85             if ( ! $self->_tables_exist() )
86             {
87             die "tables don't exist on $self->{db_descriptor}: try running $self->create_tables()\n";
88             }
89              
90             $self->{last_update_sth} = $self->{dbh}->prepare(
91             "SELECT ts FROM $self->{device_table_name} ORDER BY ts DESC LIMIT 1"
92             );
93             $self->{user_agents_sth} = $self->{dbh}->prepare(
94             "SELECT DISTINCT user_agent FROM $self->{device_table_name}"
95             );
96             $self->{devices_sth} = $self->{dbh}->prepare(
97             "SELECT * FROM $self->{device_table_name}"
98             );
99             $self->{device_sth} = $self->{dbh}->prepare(
100             "SELECT * FROM $self->{device_table_name} WHERE id = ?"
101             );
102             $self->{deviceid_sth} = $self->{dbh}->prepare(
103             "SELECT id FROM $self->{device_table_name} WHERE user_agent = ?"
104             );
105             $self->{lookup_sth} = $self->{dbh}->prepare(
106             "SELECT * FROM $self->{capability_table_name} WHERE name = ? AND deviceid = ?"
107             );
108             $self->{fall_back_sth} = $self->{dbh}->prepare(
109             "SELECT fall_back FROM $self->{device_table_name} WHERE id = ?"
110             );
111             $self->{groups_sth} = $self->{dbh}->prepare(
112             "SELECT DISTINCT groupid FROM $self->{capability_table_name}"
113             );
114             $self->{group_capabilities_sth} = $self->{dbh}->prepare(
115             "SELECT DISTINCT name FROM $self->{capability_table_name} WHERE groupid = ?"
116             );
117             $self->{capabilities_sth} = $self->{dbh}->prepare(
118             "SELECT DISTINCT name FROM $self->{capability_table_name}"
119             );
120             for my $table ( keys %tables )
121             {
122             next if $self->{$table}{sth};
123             my @fields = @{$tables{$table}};
124             my $fields = join( ",", @fields );
125             my $placeholders = join( ",", map "?", @fields );
126             my $sql = "INSERT INTO $table ( $fields ) VALUES ( $placeholders ) ";
127             $self->{$table}{sth} = $self->{dbh}->prepare( $sql );
128             }
129             $self->{initialised} = 1;
130             }
131              
132             sub set
133             {
134             my $self = shift;
135             my $opt = shift;
136             my $val = shift;
137              
138             die "unknown option $opt\n" unless exists $self->{$opt};
139             return $self->{$opt} = $val;
140             }
141              
142             sub get
143             {
144             my $self = shift;
145             my $opt = shift;
146              
147             die "unknown option $opt\n" unless exists $self->{$opt};
148             return $self->{$opt};
149             }
150              
151             sub create_tables
152             {
153             my $self = shift;
154             my $sql = shift;
155             unless ( $sql )
156             {
157             my $tt = Template->new();
158             my $template = join( '', );
159             $tt->process( \$template, $self, \$sql ) or die $tt->error;
160             }
161             for my $statement ( split( /\s*;\s*/, $sql ) )
162             {
163             next unless $statement =~ /\S/;
164             $self->{dbh}->do( $statement ) or die "$statement failed\n";
165             }
166             }
167              
168             sub touch( $$ )
169             {
170             my $path = shift;
171             my $time = shift;
172             die "no path" unless $path;
173             die "no time" unless $time;
174             print STDERR "touch $path ($time)\n";
175             return utime( $time, $time, $path );
176             }
177              
178             sub last_update
179             {
180             my $self = shift;
181             $self->_init();
182             $self->{last_update_sth}->execute();
183             my ( $ts ) = str2time($self->{last_update_sth}->fetchrow());
184             $ts ||= 0;
185             print STDERR "last update: $ts\n";
186             return $ts;
187             }
188              
189             sub rebuild_tables
190             {
191             my $self = shift;
192              
193             my $local = ($self->get_local_stats())[1];
194             my $last_update = $self->last_update();
195             if ( $local <= $last_update )
196             {
197             print STDERR "$self->{wurfl_file} has not changed since the last database update\n";
198             return 0;
199             }
200             print STDERR "$self->{wurfl_file} is newer than the last database update\n";
201             print STDERR "flush dB tables ...\n";
202             $self->{dbh}->begin_work;
203             $self->{dbh}->do( "DELETE FROM $self->{device_table_name}" );
204             $self->{dbh}->do( "DELETE FROM $self->{capability_table_name}" );
205             my ( $device_id, $group_id );
206             print STDERR "create XML parser ...\n";
207             my $xp = new XML::Parser(
208             Style => "Object",
209             Handlers => {
210             Start => sub {
211             my ( $expat, $element, %attrs ) = @_;
212             if ( $element eq 'group' )
213             {
214             my %group = %attrs;
215             $group_id = $group{id};
216             }
217             if ( $element eq 'device' )
218             {
219             my %device = %attrs;
220             my @keys = @{$tables{device}};
221             my @values = @device{@keys};
222             $device_id = $device{id};
223             $self->{device}{sth}->execute( @values );
224             }
225             if ( $element eq 'capability' )
226             {
227             my %capability = %attrs;
228             my @keys = @{$tables{capability}};
229             $capability{deviceid} = $device_id;
230             $capability{groupid} = $group_id;
231             my @values = @capability{@keys};
232             $self->{capability}{sth}->execute( @values );
233             }
234             },
235             }
236             );
237             print STDERR "parse XML ...\n";
238             $xp->parsefile( $self->{wurfl_file} );
239             print STDERR "commit dB ...\n";
240             $self->{dbh}->commit;
241             return 1;
242             }
243              
244             sub update
245             {
246             my $self = shift;
247             print STDERR "get wurfl\n";
248             my $got_wurfl = $self->get_wurfl();
249             print STDERR "got wurfl: $got_wurfl\n";
250             my $rebuilt ||= $self->rebuild_tables();
251             print STDERR "rebuilt: $rebuilt\n";
252             return $got_wurfl || $rebuilt;
253             }
254              
255             sub get_local_stats
256             {
257             my $self = shift;
258             return ( 0, 0 ) unless -e $self->{wurfl_file};
259             print STDERR "stat $self->{wurfl_file} ...\n";
260             my @stat = ( stat $self->{wurfl_file} )[ 7,9 ];
261             print STDERR "@stat\n";
262             return @stat;
263             }
264              
265             sub get_remote_stats
266             {
267             my $self = shift;
268             print STDERR "HEAD $self->{wurfl_url} ...\n";
269             my $response = $self->{ua}->head( $self->{wurfl_url} );
270             die $response->status_line unless $response->is_success;
271             die "can't get content_length\n" unless $response->content_length;
272             die "can't get last_modified\n" unless $response->last_modified;
273             my @stat = ( $response->content_length, $response->last_modified );
274             print STDERR "@stat\n";
275             return @stat;
276             }
277              
278             sub get_wurfl
279             {
280             my $self = shift;
281             my @local = $self->get_local_stats();
282             my @remote = $self->get_remote_stats();
283            
284             if ( $local[1] == $remote[1] )
285             {
286             print STDERR "@local and @remote are the same\n";
287             return 0;
288             }
289             print STDERR "@local and @remote are different\n";
290             print STDERR "GET $self->{wurfl_url} -> $self->{wurfl_file} ...\n";
291              
292             #create a temp filename
293             my $tempfile = "$self->{wurfl_home}/wurfl_$$";
294            
295             my $response = $self->{ua}->get(
296             $self->{wurfl_url},
297             ':content_file' => $tempfile
298             );
299             die $response->status_line unless $response->is_success;
300             if ($response->{_headers}->header('content-type') eq 'application/x-gzip') {
301             gunzip($tempfile => $self->{wurfl_file}) || die "gunzip failed: $GunzipError\n";
302             unlink($tempfile);
303             } elsif ($response->{_headers}->header('content-type') eq 'application/zip') {
304             unzip($tempfile => $self->{wurfl_file}) || die "unzip failed: $UnzipError\n";
305             unlink($tempfile);
306             } else {
307             move($tempfile, $self->{wurfl_file});
308             }
309             touch( $self->{wurfl_file}, $remote[1] );
310             return 1;
311             }
312              
313             sub user_agents
314             {
315             my $self = shift;
316             $self->_init();
317             $self->{user_agents_sth}->execute();
318             return map $_->[0], @{$self->{user_agents_sth}->fetchall_arrayref()};
319             }
320              
321             sub devices
322             {
323             my $self = shift;
324             $self->_init();
325             $self->{devices_sth}->execute();
326             return @{$self->{devices_sth}->fetchall_arrayref( {} )};
327             }
328              
329             sub groups
330             {
331             my $self = shift;
332             $self->_init();
333             $self->{groups_sth}->execute();
334             return map $_->[0], @{$self->{groups_sth}->fetchall_arrayref()};
335             }
336              
337             sub capabilities
338             {
339             my $self = shift;
340             my $group = shift;
341             $self->_init();
342             if ( $group )
343             {
344             $self->{group_capabilities_sth}->execute( $group );
345             return map $_->[0], @{$self->{group_capabilities_sth}->fetchall_arrayref()};
346             }
347             $self->{capabilities_sth}->execute();
348             return map $_->[0], @{$self->{capabilities_sth}->fetchall_arrayref()};
349             }
350              
351             sub _lookup
352             {
353             my $self = shift;
354             my $deviceid = shift;
355             my $name = shift;
356             $self->_init();
357             $self->{lookup_sth}->execute( $name, $deviceid );
358             return $self->{lookup_sth}->fetchrow_hashref;
359             }
360              
361             sub _fallback
362             {
363             my $self = shift;
364             my $deviceid = shift;
365             my $name = shift;
366             $self->_init();
367             my $row = $self->_lookup( $deviceid, $name );
368             return $row if $row && ( $row->{value} || $row->{deviceid} eq 'generic' );
369             $self->{fall_back_sth}->execute( $deviceid );
370             my $fallback = $self->{fall_back_sth}->fetchrow
371             || die "no fallback for $deviceid\n"
372             ;
373             if ( $fallback eq 'root' )
374             {
375             die "fellback all the way to root: this shouldn't happen\n";
376             }
377             return $self->_fallback( $fallback, $name );
378             }
379              
380             sub canonical_ua
381             {
382             no warnings 'recursion';
383             my $self = shift;
384             my $ua = shift;
385             $self->_init();
386             $self->{deviceid_sth}->execute( $ua );
387             my $deviceid = $self->{deviceid_sth}->fetchrow;
388             if ( $deviceid )
389             {
390             print STDERR "$ua found\n";
391             return $ua;
392             }
393             $ua = substr( $ua, 0, -1 );
394             # $ua =~ s/^(.+)\/(.*)$/$1\// ;
395             unless ( length $ua )
396             {
397             print STDERR "can't find canonical user agent\n";
398             return;
399             }
400             return $self->canonical_ua( $ua );
401             }
402              
403             sub device
404             {
405             my $self = shift;
406             my $deviceid = shift;
407             $self->_init();
408             $self->{device_sth}->execute( $deviceid );
409             my $device = $self->{device_sth}->fetchrow_hashref;
410             print STDERR "can't find device for user deviceid $deviceid\n" unless $device;
411             return $device;
412             }
413              
414             sub deviceid
415             {
416             my $self = shift;
417             my $ua = shift;
418             $self->_init();
419             $self->{deviceid_sth}->execute( $ua );
420             my $deviceid = $self->{deviceid_sth}->fetchrow;
421             print STDERR "can't find device id for user agent $ua\n" unless $deviceid;
422             return $deviceid;
423             }
424              
425             sub lookup
426             {
427             my $self = shift;
428             my $ua = shift;
429             my $name = shift;
430             $self->_init();
431             my %opts = @_;
432             my $deviceid = $self->deviceid( $ua );
433             return unless $deviceid;
434             return
435             $opts{no_fall_back} ?
436             $self->_lookup( $deviceid, $name )
437             :
438             $self->_fallback( $deviceid, $name )
439             ;
440             }
441              
442             sub lookup_value
443             {
444             my $self = shift;
445             $self->_init();
446             my $row = $self->lookup( @_ );
447             return $row ? $row->{value} : undef;
448             }
449              
450             sub cleanup
451             {
452             my $self = shift;
453             print STDERR "cleanup ...\n";
454             if ( $self->{dbh} )
455             {
456             print STDERR "drop tables\n";
457             for ( keys %tables )
458             {
459             print STDERR "DROP TABLE IF EXISTS $_\n";
460             $self->{dbh}->do( "DROP TABLE IF EXISTS $_" );
461             }
462             }
463             return unless $self->{wurfl_file};
464             return unless -e $self->{wurfl_file};
465             print STDERR "unlink $self->{wurfl_file}\n";
466             unlink $self->{wurfl_file} || die "Can't remove $self->{wurfl_file}: $!\n";
467             }
468              
469             #------------------------------------------------------------------------------
470             #
471             # Start of POD
472             #
473             #------------------------------------------------------------------------------
474              
475             =head1 NAME
476              
477             Mobile::Wurfl - a perl module interface to WURFL (the Wireless Universal Resource File - L).
478              
479             =head1 SYNOPSIS
480              
481             my $wurfl = Mobile::Wurfl->new(
482             wurfl_home => "/path/to/wurfl/home",
483             db_descriptor => "DBI:mysql:database=wurfl:host=localhost",
484             db_username => 'wurfl',
485             db_password => 'wurfl',
486             wurfl_url => q{http://sourceforge.net/projects/wurfl/files/WURFL/latest/wurfl-latest.xml.gz/download},
487             );
488              
489             my $dbh = DBI->connect( $db_descriptor, $db_username, $db_password );
490             my $wurfl = Mobile::Wurfl->new( dbh => $dbh );
491              
492             my $desc = $wurfl->get( 'db_descriptor' );
493             $wurfl->set( wurfl_home => "/another/path" );
494              
495             $wurfl->create_tables( $sql );
496             $wurfl->update();
497             $wurfl->get_wurfl();
498             $wurfl->rebuild_tables();
499              
500             my @devices = $wurfl->devices();
501              
502             for my $device ( @devices )
503             {
504             print "$device->{user_agent} : $device->{id}\n";
505             }
506              
507             my @groups = $wurfl->groups();
508             my @capabilities = $wurfl->capabilities();
509             for my $group ( @groups )
510             {
511             @capabilities = $wurfl->capabilities( $group );
512             }
513              
514             my $ua = $wurfl->canonical_ua( "SonyEricssonK750i/R1J Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1" );
515             my $deviceid = $wurfl->deviceid( $ua );
516              
517             my $wml_1_3 = $wurfl->lookup( $ua, "wml_1_3" );
518             print "$wml_1_3->{name} = $wml_1_3->{value} : in $wml_1_3->{group}\n";
519             my $fell_back_to = wml_1_3->{deviceid};
520             my $width = $wurfl->lookup_value( $ua, "max_image_height", no_fall_back => 1 );
521             $wurfl->cleanup();
522              
523             =head1 DESCRIPTION
524              
525             Mobile::Wurfl is a perl module that provides an interface to mobile device information represented in wurfl (L). The Mobile::Wurfl module works by saving this device information in a database (preferably mysql).
526              
527             It offers an interface to create the relevant database tables from a SQL file containing "CREATE TABLE" statements (a sample is provided with the distribution). It also provides a method for updating the data in the database from the wurfl.xml file hosted at L.
528              
529             It provides methods to query the database for lists of capabilities, and groups of capabilities. It also provides a method for generating a "canonical" user agent string (see L).
530              
531             Finally, it provides a method for looking up values for particular capability / user agent combinations. By default, this makes use of the hierarchical "fallback" structure of wurfl to lookup capabilities fallback devices if these capabilities are not defined for the requested device.
532              
533             =head1 METHODS
534              
535             =head2 new
536              
537             The Mobile::Wurfl constructor takes an optional list of named options; e.g.:
538              
539             my $wurfl = Mobile::Wurfl->new(
540             wurfl_home => "/path/to/wurfl/home",
541             db_descriptor => "DBI:mysql:database=wurfl:host=localhost",
542             db_username => 'wurfl',
543             db_password => 'wurfl',
544             wurfl_url => q{http://sourceforge.net/projects/wurfl/files/WURFL/latest/wurfl-latest.xml.gz/download},,
545             verbose => 1,
546             );
547              
548             The list of possible options are as follows:
549              
550             =over 4
551              
552             =item wurfl_home
553              
554             Used to set the default home diretory for Mobile::Wurfl. This is where the cached copy of the wurfl.xml file is stored. It defaults to /tmp.
555              
556             =item db_descriptor
557              
558             A database descriptor - as used by L to define the type, host, etc. of database to connect to. This is where the data from wurfl.xml will be stored, in two tables - device and capability. The default is "DBI:mysql:database=wurfl:host=localhost" (i.e. a mysql database called wurfl, hosted on localhost.
559              
560             =item db_username
561              
562             The username used to connect to the database defined by L. Default is "wurfl".
563              
564             =item db_password
565              
566             The password used to connect to the database defined by L. Default is "wurfl".
567              
568             =item dbh
569              
570             A DBI database handle.
571              
572             =item wurfl_url
573              
574             The URL from which to get the wurfl.xml file, this can be uncompressed or compressed with zip or gzip Default is L.
575              
576             =item verbose
577              
578             If set to a true value, various status messages will be output. If value is 1, these messages will be written to a logfile called wurfl.log in L, if > 1 to STDERR.
579              
580             =back
581              
582             =head2 set / get
583              
584             The set and get methods can be used to set / get values for the constructor options described above. Their usage is self explanatory:
585              
586             my $desc = $wurfl->get( 'db_descriptor' );
587             $wurfl->set( wurfl_home => "/another/path" );
588              
589             =head2 create_tables
590              
591             The create_tables method is used to create the database tables required for Mobile::Wurfl to store the wurfl.xml data in. It can be passed as an argument a string containing appropriate SQL "CREATE TABLE" statements. If this is not passed, it uses appropriate statements for a mysql database (see __DATA__ section of the module for the specifics). This should only need to be called as part of the initial configuration.
592              
593             =head2 update
594              
595             The update method is called to update the database tables with the latest information from wurfl.xml. It calls get_wurfl, and then rebuild_tables, each of which work out what if anything needs to be done (see below). It returns true if there has been an update, and false otherwise.
596              
597             =head2 rebuild_tables
598              
599             The rebuild_tables method is called by the update method. It checks the modification time of the locally cached copy of the wurfl.xml file against the last modification time on the database, and if it is greater, rebuilds the database table from the wurfl.xml file.
600              
601             =head2 get_wurfl
602              
603             The get_wurfl method is called by the update method. It checks to see if the locally cached version of the wurfl.xml file is up to date by doing a HEAD request on the WURFL URL, and comparing modification times. If there is a newer version of the file at the WURFL URL, or if the locally cached file does not exist, then the module will GET the wurfl.xml file from the WURFL URL.
604              
605             =head2 devices
606              
607             This method returns a list of all the devices in WURFL. This is returned as a list of hashrefs, each of which has keys C, C, C, and C.
608              
609             =head2 groups
610              
611             This method returns a list of the capability groups in WURFL.
612              
613             =head2 capabilities( group )
614              
615             This method returns a list of the capabilities in a group in WURFL. If no group is given, it returns a list of all the capabilites.
616              
617             =head2 canonical_ua( ua_string )
618              
619             This method takes a user agent string as an argument, and tries to find a matching "canonical" user agent in WURFL. It does this simply by recursively doing a lookup on the string, and if this fails, chopping anything after and including the last "/" in the string. So, for example, for the user agent string:
620              
621             SonyEricssonK750i/R1J Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1
622              
623             the canonical_ua method would try the following:
624              
625             SonyEricssonK750i/R1J Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration/CLDC-1.1
626             SonyEricssonK750i/R1J Browser/SEMC-Browser/4.2 Profile/MIDP-2.0 Configuration
627             SonyEricssonK750i/R1J Browser/SEMC-Browser/4.2 Profile
628             SonyEricssonK750i/R1J Browser/SEMC-Browser
629             SonyEricssonK750i/R1J Browser
630             SonyEricssonK750i
631              
632             until it found a user agent string in WURFL, and then return it (or return undef if none were found). In the above case (for WURFL v2.0) it returns the string "SonyEricssonK750i".
633              
634             =head2 deviceid( ua_string )
635              
636             This method returns the deviceid for a given user agent string.
637              
638             =head2 device( deviceid )
639              
640             This method returns a hashref for a given deviceid. The hashref has keys C, C, C, and C.
641              
642             =head2 lookup( ua_string, capability, [ no_fall_back => 1 ] )
643              
644             This method takes a user agent string and a capability name, and returns a hashref representing the capability matching this combination. The hashref has the keys C, C, C and C. By default, if a capability has no value for that device, it recursively falls back to its fallback device, until it does find a value. You can discover the device "fallen back to" by accessing the C key of the hash. This behaviour can be controlled by using the "no_fall_back" option.
645              
646             =head2 lookup_value( ua_string, capability, [ no_fall_back => 1 ] )
647              
648             This method is similar to the lookup method, except that it returns a value instead if a hash.
649              
650             =head2 cleanup()
651              
652             This method forces the module to C all of the database tables it has created, and remove the locally cached copy of wurfl.xml.
653              
654             =head1 AUTHOR
655              
656             Ave Wrigley
657              
658             =head1 COPYRIGHT
659              
660             Copyright (c) 2004 Ave Wrigley. All rights reserved. This program is free
661             software; you can redistribute it and/or modify it under the same terms as Perl
662             itself.
663              
664             =cut
665              
666             #------------------------------------------------------------------------------
667             #
668             # End of POD
669             #
670             #------------------------------------------------------------------------------
671              
672             #------------------------------------------------------------------------------
673             #
674             # True ...
675             #
676             #------------------------------------------------------------------------------
677              
678             1;
679              
680             __DATA__