File Coverage

blib/lib/Bio/Das/Lite.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #########
2             # Author: rpettett@cpan.org
3             # Maintainer: rpettett@cpan.org
4             # Created: 2005-08-23
5             # Last Modified: $Date: 2011-05-06 11:18:40 +0100 (Fri, 06 May 2011) $ $Author: zerojinx $
6             # Source: $Source: /var/lib/cvsd/cvsroot/Bio-DasLite/Bio-DasLite/lib/Bio/Das/Lite.pm,v $
7             # Id: $Id: Lite.pm 53 2011-05-06 10:18:40Z zerojinx $
8             # $HeadURL $
9             #
10             package Bio::Das::Lite;
11 18     18   433568 use strict;
  18         45  
  18         643  
12 18     18   93 use warnings;
  18         36  
  18         517  
13 18     18   127541 use WWW::Curl::Multi;
  0            
  0            
14             use WWW::Curl::Easy; # CURLOPT imports
15             use HTTP::Response;
16             use Carp;
17             use English qw(-no_match_vars);
18             use Readonly;
19              
20             our $DEBUG = 0;
21             our $VERSION = '2.11';
22             Readonly::Scalar our $TIMEOUT => 5;
23             Readonly::Scalar our $REG_TIMEOUT => 15;
24             Readonly::Scalar our $LINKRE => qr{]*?>([^<]*)|]*?/>}smix;
25             Readonly::Scalar our $NOTERE => qr{]*>([^<]*)}smix;
26             Readonly::Scalar our $DAS_STATUS_TEXT => {
27             200 => '200 OK',
28             400 => '400 Bad command (command not recognized)',
29             401 => '401 Bad data source (data source unknown)',
30             402 => '402 Bad command arguments (arguments invalid)',
31             403 => '403 Bad reference object',
32             404 => '404 Requested object unknown',
33             405 => '405 Coordinate error',
34             500 => '500 Server error',
35             501 => '501 Unimplemented feature',
36             };
37              
38             #########
39             # $ATTR contains information about document structure - tags, attributes and subparts
40             # This is split up by call to reduce the number of tag passes for each response
41             #
42             our %COMMON_STYLE_ATTRS = (
43             zindex => [],
44             height => [],
45             fgcolor => [],
46             bgcolor => [],
47             label => [],
48             bump => [],
49             );
50             our %SCORED_STYLE_ATTRS = (
51             min => [],
52             max => [],
53             steps => [],
54             color1 => [],
55             color2 => [],
56             color3 => [],
57             height => [],
58             );
59             our $ATTR = {
60             '_segment' => {
61             'segment' => [qw(id start stop version label)],
62             },
63             # feature notes and links are special cases and taken care of elsewhere
64             'feature' => {
65             'feature' => [qw(id label)],
66             'method' => [qw(id cvId)],
67             'type' => [qw(id category reference subparts superparts cvId)],
68             'target' => [qw(id start stop)],
69             'start' => [],
70             'end' => [],
71             'orientation' => [],
72             'phase' => [],
73             'score' => [],
74             'parent' => { 'parent' => [qw(id)] },
75             'part' => { 'part' => [qw(id)] },
76             },
77             'sequence' => {
78             'sequence' => [qw(id start stop version label)],
79             },
80             # NOTE: The dna command is deprecated:
81             'dna' => {
82             'sequence' => {
83             'sequence' => [qw(id start stop version)],
84             'dna' => [qw(length)],
85             },
86             },
87             'entry_points' => {
88             'entry_points' => [qw(href total start end)],
89             'segment' => {
90             'segment' => [qw(id start stop type orientation subparts version)],
91             },
92             },
93             # NOTE: The dsn command is deprecated:
94             'dsn' => {
95             'dsn' => [],
96             'source' => [qw(id)],
97             'mapmaster' => [],
98             'description' => [],
99             },
100             'type' => {
101             'type' => [qw(id category cvId)],
102             'segment' => [qw(id start stop version label)],
103             },
104             'alignment' => {
105             'alignment' => [qw(name alignType max)],
106             'alignobject' => {
107             'alignobject' => [qw(objVersion
108             intObjectId
109             type
110             dbSource
111             dbVersion
112             dbAccessionId
113             dbCoordSys)],
114             'alignobjectdetail' => {
115             'alignobjectdetail' => [qw(dbSource
116             property)],
117             },
118             'sequence' => [],
119             },
120             'score' => [qw(score)],
121             'block' => {
122             'block' => [qw(blockOrder)],
123             'segment' => {
124             'segment' => [qw(intObjectId
125             start
126             end
127             orientation)],
128             'cigar' => [],
129             },
130             },
131             },
132              
133             'structure' => {
134             'object' => [qw(dbAccessionId
135             inObjectId
136             objectVersion
137             type
138             dbSource
139             dbVersion
140             dbCoordSys)],
141             'chain' => {
142             'chain' => [qw(id SwissprotId model)],
143             'group' => {
144             'group' => [qw(name type groupID)],
145             'atom' => {
146             'atom' => [qw(atomID
147             occupancy
148             tempFactor
149             altLoc
150             atomName
151             x y z)]
152             },
153             },
154             },
155             'het' => {
156             'group' => {
157             'group' => [qw(name type groupID)],
158             'atom' => {
159             'atom' => [qw(atomId
160             occupancy
161             tempFactor
162             altLoc
163             atomName
164             x y z)]
165             },
166             },
167             },
168             'connect' => {
169             'connect' => [qw(atomSerial type)],
170             'atomID' => {
171             'atomID' => [qw(atomID)],
172             },
173             },
174             },
175             'sources' => {
176             'source' => {
177             'source' => [qw(uri title doc_href description)],
178             'maintainer' => {
179             'maintainer' => [qw(email)],
180             },
181             'version' => {
182             'version' => [qw(uri created)],
183             'coordinates' => {
184             'coordinates' => [qw(uri
185             source
186             authority
187             taxid
188             test_range
189             version)],
190             },
191             'capability' => {
192             'capability' => [qw(type query_uri)],
193             },
194             'prop' => {
195             'prop' => [qw(name value)],
196             },
197             },
198             },
199             },
200             'stylesheet' => {
201             'stylesheet' => [qw(version)],
202             'category' => {
203             'category' => [qw(id)],
204             'type' => {
205             'type' => [qw(id)],
206             'glyph' => {
207             'glyph' => [qw(zoom)],
208             'arrow' => {
209             'parallel' => [],
210             'southwest' => [],
211             'northeast' => [],
212             %COMMON_STYLE_ATTRS,
213             },
214             'anchored_arrow' => {
215             'parallel' => [],
216             %COMMON_STYLE_ATTRS,
217             },
218             'box' => {
219             'linewidth' => [],
220             'pattern' => [], # WTSI extension
221             %COMMON_STYLE_ATTRS,
222             },
223             'cross' => {
224             %COMMON_STYLE_ATTRS,
225             },
226             'dot' => \%COMMON_STYLE_ATTRS,
227             'ex' => {
228             %COMMON_STYLE_ATTRS,
229             },
230             'hidden' => {},
231             'line' => {
232             'style' => [],
233             %COMMON_STYLE_ATTRS,
234             },
235             'span' => {
236             %COMMON_STYLE_ATTRS,
237             },
238             'text' => {
239             'font' => [],
240             'fontsize' => [],
241             'string' => [],
242             #'style' => [], HANDLED SEPARATELY
243             'fgcolor' => [],
244             'bgcolor' => [],
245             'label' => [],
246             'bump' => [],
247             },
248             'primers' => \%COMMON_STYLE_ATTRS,
249             'toomany' => {
250             'linewidth' => [],
251             %COMMON_STYLE_ATTRS,
252             },
253             'triangle' => {
254             'linewidth' => [],
255             'direction' => [],
256             %COMMON_STYLE_ATTRS,
257             },
258             'gradient' => \%SCORED_STYLE_ATTRS,
259             'histogram' => \%SCORED_STYLE_ATTRS,
260             'lineplot' => \%SCORED_STYLE_ATTRS,
261             },
262             },
263             },
264             },
265             };
266              
267             #########
268             # $OPTS contains information about parameters to use for queries
269             #
270             our $OPTS = {
271             'feature' => [qw(segment type category categorize feature_id maxbins)],
272             'type' => [qw(segment)],
273             'sequence' => [qw(segment)],
274             'dna' => [qw(segment)],
275             'entry_points' => [qw(rows)],
276             'dsn' => [],
277             'sources' => [],
278             'stylesheet' => [],
279             'alignment' => [qw(query rows subject subjectcoordsys)],
280             'structure' => [qw(query)],
281             };
282              
283             sub new {
284             my ($class, $ref) = @_;
285             $ref ||= {};
286             my $self = {
287             'dsn' => [],
288             'timeout' => $TIMEOUT,
289             'data' => {},
290             'caching' => 1,
291             'registry' => [qw(http://www.dasregistry.org/das)],
292             '_registry_sources' => [],
293             };
294              
295             bless $self, $class;
296              
297             if($ref && ref $ref) {
298             for my $arg (qw(dsn timeout caching callback registry user_agent
299             http_proxy proxy_user proxy_pass no_proxy)) {
300             if(exists $ref->{$arg} && $self->can($arg)) {
301             $self->$arg($ref->{$arg});
302             }
303             }
304             } elsif($ref) {
305             $self->dsn($ref);
306             }
307              
308             return $self;
309             }
310              
311             sub new_from_registry {
312             my ($class, $ref) = @_;
313             my $user_timeout = defined $ref->{timeout} ? 1 : 0;
314             my $self = $class->new($ref);
315             # If the user specifies a timeout, use it.
316             # But if not, temporarily increase the timeout for the registry request.
317             if (!$user_timeout) {
318             $self->timeout($REG_TIMEOUT);
319             }
320             my $sources = $self->registry_sources($ref);
321             # And reset it back to the "normal" non-registry timeout.
322             if (!$user_timeout) {
323             $self->timeout($TIMEOUT);
324             }
325             $self->dsn([map { $_->{'url'} } @{$sources}]);
326             return $self;
327             }
328              
329             # We implement this method because LWP does not parse user/password
330             sub http_proxy {
331             my ($self, $proxy) = @_;
332             if($proxy) {
333             $self->{'http_proxy'} = $proxy;
334             }
335              
336             if(!$self->{'_checked_http_proxy_env'}) {
337             $self->{'http_proxy'} ||= $ENV{'http_proxy'} || q();
338             $self->{'_checked_http_proxy_env'} = 1;
339             }
340              
341             if($self->{'http_proxy'} =~ m{^(https?://)(\S+):(.*?)\@(.*?)$}smx) {
342             #########
343             # http_proxy contains username & password - we'll set them up here:
344             #
345             $self->proxy_user($2);
346             $self->proxy_pass($3);
347              
348             $self->{'http_proxy'} = "$1$4";
349             }
350              
351             return $self->{'http_proxy'};
352             }
353              
354             sub no_proxy {
355             my ($self, @args) = @_;
356              
357             if (scalar @args) {
358             if ($args[0] && ref $args[0] && ref $args[0] eq 'ARRAY') {
359             $self->{'no_proxy'} = $args[0];
360             } else {
361             $self->{'no_proxy'} = \@args;
362             }
363             }
364              
365             if(!$self->{'_checked_no_proxy_env'}) {
366             $self->{'no_proxy'} ||= [split /\s*,\s*/smx, $ENV{'no_proxy'} || q()];
367             $self->{'_checked_no_proxy_env'} = 1;
368             }
369              
370             return $self->{'no_proxy'} || [];
371             }
372              
373             sub _get_set {
374             my ($self, $key, $value) = @_;
375             if(defined $value) {
376             $self->{$key} = $value;
377             }
378             return $self->{$key};
379             }
380              
381             sub proxy_user {
382             my ($self, $val) = @_;
383             return $self->_get_set('proxy_user', $val);
384             }
385              
386             sub proxy_pass {
387             my ($self, $val) = @_;
388             return $self->_get_set('proxy_pass', $val);
389             }
390              
391             sub user_agent {
392             my ($self, $val) = @_;
393             return $self->_get_set('user_agent', $val) || "Bio::Das::Lite v$VERSION";
394             }
395              
396             sub timeout {
397             my ($self, $val) = @_;
398             return $self->_get_set('timeout', $val);
399             }
400              
401             sub caching {
402             my ($self, $val) = @_;
403             return $self->_get_set('caching', $val);
404             }
405              
406             sub max_hosts {
407             my ($self, $val) = @_;
408             carp 'WARNING: max_hosts method is decprecated and has no effect';
409             return $self->_get_set('_max_hosts', $val);
410             }
411              
412             sub max_req {
413             my ($self, $val) = @_;
414             carp 'WARNING: max_req method is decprecated and has no effect';
415             return $self->_get_set('_max_req', $val);
416             }
417              
418             sub callback {
419             my ($self, $val) = @_;
420             return $self->_get_set('callback', $val);
421             }
422              
423             sub basename {
424             my ($self, $dsn) = @_;
425             $dsn ||= $self->dsn();
426             my @dsns = (ref $dsn)?@{$dsn}:$dsn;
427             my @res = ();
428              
429             for my $service (@dsns) {
430             $service =~ m{(https?://.*/das)/?}smx;
431             if($1) {
432             push @res, $1;
433             }
434             }
435              
436             return \@res;
437             }
438              
439             sub dsn {
440             my ($self, $dsn) = @_;
441             if($dsn) {
442             if(ref $dsn eq 'ARRAY') {
443             $self->{'dsn'} = $dsn;
444             } else {
445             $self->{'dsn'} = [$dsn];
446             }
447             }
448             return $self->{'dsn'};
449             }
450              
451             sub dsns {
452             my ($self, $query, $opts) = @_;
453             $opts ||= {};
454             $opts->{'use_basename'} = 1;
455             return $self->_generic_request($query, 'dsn', $opts);
456             }
457              
458             sub entry_points {
459             my ($self, $query, $opts) = @_;
460             return $self->_generic_request($query, 'entry_points', $opts);
461             }
462              
463              
464             sub types {
465             my ($self, $query, $opts) = @_;
466             return $self->_generic_request($query, 'type(s)', $opts);
467             }
468              
469             sub features {
470             my ($self, $query, $callback, $opts) = @_;
471             if(ref $callback eq 'HASH' && !defined $opts) {
472             $opts = $callback;
473             undef $callback;
474             }
475             if($callback) {
476             $self->{'callback'} = $callback;
477             }
478             return $self->_generic_request($query, 'feature(s)', $opts);
479             }
480              
481             sub sequence {
482             my ($self, $query, $opts) = @_;
483             return $self->_generic_request($query, 'sequence', $opts);
484             }
485              
486             sub dna {
487             my ($self, $query, $opts) = @_;
488             return $self->_generic_request($query, 'dna', $opts);
489             }
490              
491             sub alignment {
492             my ($self, $opts) = @_;
493             return $self->_generic_request($opts, 'alignment');
494             }
495              
496             sub structure {
497             my ($self, $opts) = @_;
498             return $self->_generic_request($opts, 'structure');
499             }
500              
501             sub sources {
502             my ($self, $opts) = @_;
503             return $self->_generic_request($opts, 'sources');
504             }
505              
506             sub stylesheet {
507             my ($self, $callback, $opts) = @_;
508             if(ref $callback eq 'HASH' && !defined $opts) {
509             $opts = $callback;
510             undef $callback;
511             }
512             if($callback) {
513             $self->{'callback'} = $callback;
514             }
515             return $self->_generic_request(undef, 'stylesheet', $opts);
516             }
517              
518             #########
519             # Private methods
520             #
521              
522             #########
523             # Build the query URL; perform an HTTP fetch; drop into the recursive parser; apply any post-processing
524             #
525             sub _generic_request {
526             my ($self, $query, $fname, $opts) = @_;
527             $opts ||= {};
528             delete $self->{'currentsegs'};
529             my $results = {};
530             my $reqname = $fname;
531             $reqname =~ s/(?:[(]|[)])//smxg;
532             ($fname) = $fname =~ /^([[:lower:]_]+)/smx;
533              
534             my $ref = $self->build_requests({
535             query => $query,
536             fname => $fname,
537             reqname => $reqname,
538             opts => $opts,
539             results => $results
540             });
541              
542             $self->_fetch($ref, $opts->{'headers'});
543             $DEBUG and print {*STDERR} qq(Content retrieved\n);
544              
545             $self->postprocess($fname, $results);
546              
547             #########
548             # deal with caching
549             #
550             if($self->{'caching'}) {
551             $DEBUG and print {*STDERR} qq(Performing cache handling\n);
552             for my $s (keys %{$results}) {
553             if($DEBUG && !$results->{$s}) {
554             print {*STDERR} qq(CACHE HIT for $s\n); ## no critic (InputOutput::RequireCheckedSyscalls)
555             }
556             $results->{$s} ||= $self->{'_cache'}->{$s};
557             $self->{'_cache'}->{$s} ||= $results->{$s};
558             }
559             }
560              
561             return $results;
562             }
563              
564             sub build_queries {
565             my ($self, $query, $fname) = @_;
566             my @queries;
567              
568             if($query) {
569             if(ref $query eq 'HASH') {
570             #########
571             # If the query param was a hashref, stitch the parts together
572             #
573             push @queries, join q(;), map { "$_=$query->{$_}" } grep { $query->{$_} } @{$OPTS->{$fname}};
574              
575             } elsif(ref $query eq 'ARRAY') {
576             #########
577             # If the query param was an arrayref
578             #
579              
580             if(ref $query->[-1] eq 'CODE') {
581             #########
582             # ... and the last arg is a code-block, set up the callback for this run and remove the arg
583             #
584             $self->callback($query->[-1]);
585             pop @{$query};
586             }
587              
588             if(ref $query->[0] eq 'HASH') {
589             #########
590             # ... or if the first array arg is a hash, stitch the series of queries together
591             #
592             push @queries, map { ## no critic (ProhibitComplexMappings)
593             my $q = $_;
594             join q(;), map { "$_=$q->{$_}" } grep { $q->{$_} } @{$OPTS->{$fname}};
595             } @{$query};
596              
597             } else {
598             #########
599             # ... but otherwise assume it's a plain segment string
600             #
601             push @queries, map { "segment=$_"; } @{$query};
602             }
603              
604             } else {
605             #########
606             # and if it wasn't a hashref or an arrayref, then assume it's a plain segment string
607             #
608             push @queries, "segment=$query";
609             }
610              
611             } else {
612             #########
613             # Otherwise we've no idea what you're trying to do
614             #
615             push @queries, q();
616             }
617             return \@queries;
618             }
619              
620             sub _hack_fname {
621             my ($self, $fname) = @_;
622             #########
623             # Sucky hacks
624             #
625             if($fname eq 'structure') {
626             $fname = 'dasstructure';
627             } elsif($fname eq 'dna') {
628             $fname = 'sequence';
629             }
630             return $fname;
631             }
632              
633             sub build_requests {
634             my ($self, $args) = @_;
635             my $query = $args->{query};
636             my $fname = $args->{fname};
637             my $reqname = $args->{reqname};
638             my $opts = $args->{opts};
639             my $results = $args->{results};
640             my $queries = $self->build_queries($query, $fname);
641             my $attr = $ATTR->{$fname};
642             my $dsn = $opts->{'use_basename'}?$self->basename():$self->dsn();
643             my @bn = @{$dsn};
644             my $ref = {};
645              
646             for my $bn (@bn) {
647             #########
648             # loop over dsn basenames
649             #
650             $bn =~ s/\/+$//smx;
651             for my $request (map { $_ ? "$bn/$reqname?$_" : "$bn/$reqname" } @{$queries}) {
652             #########
653             # and for each dsn, loop over the query request
654             #
655              
656             if($self->{'caching'} && $self->{'_cache'}->{$request}) {
657             #########
658             # the key has to be present, but the '0' callback will be ignored by _fetch
659             #
660             $results->{$request} = 0;
661             next;
662             }
663              
664             $results->{$request} = [];
665             $ref->{$request} = sub {
666             my $data = shift || q();
667             $self->{'data'}->{$request} .= $data;
668              
669             if(!$self->{'currentsegs'}->{$request}) {
670             #########
671             # If we haven't yet found segment information for this request
672             # Then look for some. This one is a non-destructive scan.
673             #
674             my $matches = $self->{'data'}->{$request} =~ m{(]*>)}smix;
675              
676             if($matches) {
677             my $seginfo = [];
678             $self->_parse_branch({
679             request => $request,
680             seginfo => $seginfo,
681             attr => $ATTR->{'_segment'},
682             blk => $1,
683             addseginfo => 0,
684             });
685             $self->{'currentsegs'}->{$request} = $seginfo->[0];
686             }
687             }
688              
689             if($DEBUG) {
690             print {*STDERR} qq(invoking _parse_branch for $fname\n) or croak $ERRNO;
691             }
692              
693             #########
694             # Sucky hacks
695             #
696             if($fname eq 'dna') {
697             $attr = $attr->{'sequence'};
698             }
699             $fname = $self->_hack_fname($fname);
700              
701             my $pat = qr{(<$fname.*?/$fname>|<$fname[^>]+/>)}smix;
702             while($self->{'data'}->{$request} =~ s/$pat//smx) {
703             $self->_parse_branch({
704             request => $request,
705             seginfo => $results->{$request},
706             attr => $attr,
707             blk => $1,
708             addseginfo => 1,
709             });
710             }
711              
712             if($DEBUG) {
713             print {*STDERR} qq(completed _parse_branch\n) or croak $ERRNO;
714             }
715              
716             return;
717             };
718             }
719             }
720             return $ref;
721             }
722              
723             sub postprocess {
724             my ($self, $fname, $results) = @_;
725              
726             $fname = $self->_hack_fname($fname);
727              
728             #########
729             # Add in useful segment information for empty segments
730             # In theory there should only ever be one element in @{$self->{'seginfo'}}
731             # as requests are parallelised by segment
732             #
733             for my $req (keys %{$results}) {
734             if(!$results->{$req} ||
735             scalar @{$results->{$req}} == 0) {
736             $results->{$req} = $self->{'currentsegs'}->{$req};
737             }
738             }
739              
740             #########
741             # fix ups
742             #
743             if($fname eq 'entry_points') {
744             $DEBUG and print {*STDERR} qq(Running postprocessing for entry_points\n);
745              
746             for my $s (keys %{$results}) {
747             my $res = $results->{$s} || [];
748             for my $r (@{$res}) {
749             delete $r->{'segment_id'};
750             }
751             }
752              
753             } elsif($fname eq 'sequence') {
754             $DEBUG and print {*STDERR} qq(Running postprocessing for dna\n);
755              
756             for my $s (keys %{$results}) {
757             my $res = $results->{$s} || [];
758              
759             for my $r (@{$res}) {
760             if(exists $r->{'dna'}) {
761             $r->{'dna'} =~ s/\s+//smgx;
762              
763             } elsif(exists $r->{'sequence'}) {
764             $r->{'sequence'} =~ s/\s+//smgx;
765             }
766             }
767             }
768             }
769             return;
770             }
771              
772             #########
773             # Set up the parallel HTTP fetching
774             # This uses our LWP::Parallel::UserAgent subclass which handles DAS statuses
775             #
776             sub _fetch {
777             my ($self, $url_ref, $headers) = @_;
778              
779             $self->{'statuscodes'} = {};
780             $self->{'specversions'} = {};
781             if(!$headers) {
782             $headers = {};
783             }
784              
785             if($ENV{HTTP_X_FORWARDED_FOR}) {
786             $headers->{'X-Forwarded-For'} ||= $ENV{'HTTP_X_FORWARDED_FOR'};
787             }
788             $headers->{'X-DAS-Version'} ||= '1.6';
789              
790             # Convert header pairs to strings
791             my @headers;
792             for my $h (keys %{ $headers }) {
793             push @headers, "$h: " . $headers->{$h};
794             }
795              
796             # We will now issue the actual requests. Due to insufficient support for error
797             # handling and proxies, we can't use WWW::Curl::Simple. So we generate a
798             # WWW::Curl::Easy object here, and register it with WWW::Curl::Multi.
799              
800             my $curlm = WWW::Curl::Multi->new();
801             my %reqs;
802             my $i = 0;
803              
804             # First initiate the requests
805             for my $url (keys %{$url_ref}) {
806             if(ref $url_ref->{$url} ne 'CODE') {
807             next;
808             }
809             $DEBUG and print {*STDERR} qq(Building WWW::Curl::Easy for $url [timeout=$self->{'timeout'}] via $url_ref->{$url}\n);
810              
811             $i++;
812             my $curl = WWW::Curl::Easy->new();
813              
814             $curl->setopt( CURLOPT_NOPROGRESS, 1 );
815             $curl->setopt( CURLOPT_FOLLOWLOCATION, 1 );
816             $curl->setopt( CURLOPT_USERAGENT, $self->user_agent );
817             $curl->setopt( CURLOPT_URL, $url );
818              
819             if (scalar @headers) {
820             $curl->setopt( CURLOPT_HTTPHEADER, \@headers );
821             }
822              
823             my ($body_ref, $head_ref);
824             open my $fileb, q[>], \$body_ref or croak 'Error opening data handle'; ## no critic (RequireBriefOpen)
825             $curl->setopt( CURLOPT_WRITEDATA, $fileb );
826              
827             open my $fileh, q[>], \$head_ref or croak 'Error opening header handle'; ## no critic (RequireBriefOpen)
828             $curl->setopt( CURLOPT_WRITEHEADER, $fileh );
829              
830             # we set this so we have the ref later on
831             $curl->setopt( CURLOPT_PRIVATE, $i );
832             $curl->setopt( CURLOPT_TIMEOUT, $self->timeout || $TIMEOUT );
833             #$curl->setopt( CURLOPT_CONNECTTIMEOUT, $self->connection_timeout || 2 );
834              
835             $self->_fetch_proxy_setup($curl);
836              
837             $curlm->add_handle($curl);
838              
839             $reqs{$i} = {
840             'uri' => $url,
841             'easy' => $curl,
842             'head' => \$head_ref,
843             'body' => \$body_ref,
844             };
845             }
846              
847             $DEBUG and print {*STDERR} qq(Requests submitted. Waiting for content\n);
848              
849             $self->_receive($url_ref, $curlm, \%reqs);
850              
851             return;
852             }
853              
854             sub _fetch_proxy_setup {
855             my ($self, $curl) = @_;
856              
857             if ( my $proxy = $self->http_proxy ) {
858             if ( defined $Bio::Das::Lite::{CURLOPT_PROXY} ) {
859             $curl->setopt( &CURLOPT_PROXY, $proxy ); ## no critic (ProhibitAmpersandSigils)
860             } else {
861             croak 'Trying to set a proxy, but your version of libcurl does not support this feature';
862             }
863             }
864              
865             if ( my $proxy_user = $self->proxy_user ) {
866             if ( defined $Bio::Das::Lite::{CURLOPT_PROXYUSERNAME} ) {
867             $curl->setopt( &CURLOPT_PROXYUSERNAME, $proxy_user ); ## no critic (ProhibitAmpersandSigils)
868             } else {
869             croak 'Trying to set a proxy username, but your version of libcurl does not support this feature';
870             }
871             }
872              
873             if ( my $proxy_pass = $self->proxy_pass ) {
874             if ( defined $Bio::Das::Lite::{CURLOPT_PROXYPASSWORD} ) {
875             $curl->setopt( &CURLOPT_PROXYPASSWORD, $proxy_pass ); ## no critic (ProhibitAmpersandSigils)
876             } else {
877             croak 'Trying to set a proxy password, but your version of libcurl does not support this feature';
878             }
879             }
880              
881             my @no_proxy = @{ $self->no_proxy };
882             if ( scalar @no_proxy ) {
883             if ( defined $Bio::Das::Lite::{CURLOPT_NOPROXY} ) {
884             $curl->setopt( &CURLOPT_NOPROXY, join q(,), @no_proxy ); ## no critic (ProhibitAmpersandSigils)
885             } else {
886             croak 'Trying to set proxy exclusions, but your version of libcurl does not support this feature';
887             }
888             }
889              
890             return;
891             }
892              
893             sub _receive {
894             my ($self, $url_ref, $curlm, $reqs) = @_;
895              
896             # Now check for results as they come back
897             my $i = scalar keys %{ $reqs };
898             while ($i) {
899             my $active_transfers = $curlm->perform;
900             if ($active_transfers != $i) {
901             while (my ($id,$retcode) = $curlm->info_read) {
902             $id || next;
903              
904             $i--;
905             my $req = $reqs->{$id};
906             my $uri = $req->{'uri'};
907             my $head = ${ $req->{'head'} } || q();
908             my $body = ${ $req->{'body'} } || q();
909              
910             # We got a response from the server:
911             if ($retcode == 0) {
912             my $res = HTTP::Response->parse( $head . "\n" . $body );
913             my $msg;
914              
915             # Workaround for redirects, which result in multiple headers:
916             while ($res->content =~ /^HTTP\/\d+\.\d+\s\d+/mxs) { # check for status line like "HTTP/1.1 200 OK"
917             $res = HTTP::Response->parse( $res->content );
918             }
919              
920             $self->{specversions}->{$uri} = $res->header('X-DAS-Version');
921              
922             # Prefer X-DAS-Status
923             my ($das_status) = ($res->header('X-DAS-Status') || q()) =~ m/^(\d+)/smx;
924             if ($das_status) {
925             $msg = $self->{statuscodes}->{$uri} = $DAS_STATUS_TEXT->{$das_status};
926             # just in case we get a status we don't understand:
927             $msg ||= $das_status . q( ) . ($res->message || 'Unknown status');
928             }
929             # Fall back to HTTP status
930             else {
931             $msg = $res->status_line;
932             # workaround for bug in HTTP::Response parse method:
933             $msg =~ s/\r//gsmx;
934             }
935              
936             $self->{statuscodes}->{$uri} = $msg;
937             $url_ref->{$uri}->($res->content); # run the content handling code
938             }
939             # A connection error, timeout etc (NOT an HTTP status):
940             else {
941             $self->{statuscodes}->{$uri} = '500 ' . $req->{'easy'}->strerror($retcode);
942             }
943              
944             delete($reqs->{$id}); # put out of scope to free memory
945             }
946             }
947             }
948              
949             return;
950             }
951              
952             sub statuscodes {
953             my ($self, $url) = @_;
954             $self->{'statuscodes'} ||= {};
955             return $url?$self->{'statuscodes'}->{$url}:$self->{'statuscodes'};
956             }
957              
958             sub specversions {
959             my ($self, $url) = @_;
960             $self->{'specversions'} ||= {};
961             return $url ? $self->{'specversions'}->{$url} : $self->{'specversions'};
962             }
963              
964             #########
965             # Using the $attr structure describing the structure of this branch,
966             # recursively parse the XML blocks and build the corresponding response data structure
967             #
968             sub _parse_branch {
969             my ($self, $args) = @_;
970             my $dsn = $args->{request};
971             my $ar_ref = $args->{seginfo};
972             my $attr = $args->{attr};
973             my $blk = $args->{blk};
974             my $addseginfo = $args->{addseginfo};
975             my $depth = $args->{depth} || 0;
976             my $ref = {};
977              
978             my (@parts, @subparts);
979             while(my ($k, $v) = each %{$attr}) {
980             if(ref $v eq 'HASH') {
981             push @subparts, $k;
982             } else {
983             push @parts, $k;
984             }
985             }
986              
987             #########
988             # recursive child-node handling, usually for s
989             #
990             for my $subpart (@subparts) {
991             my $subpart_ref = [];
992              
993             my $pat = qr{(<$subpart[^>]*/>|<$subpart[^>]*?(?!/)>.*?/$subpart>)}smix;
994             while($blk =~ s/$pat//smx) {
995             $self->_parse_branch({
996             request => $dsn,
997             seginfo => $subpart_ref,
998             attr => $attr->{$subpart},
999             blk => $1,
1000             addseginfo => 0,
1001             depth => $depth+1,
1002             });
1003             }
1004              
1005             if(scalar @{$subpart_ref}) {
1006             $ref->{$subpart} = $subpart_ref;
1007             }
1008              
1009             #########
1010             # To-do: normalise group data across features here - mostly for 'group' tags in feature responses
1011             # i.e. merge links, use cached hashrefs (keyed on group id) describing groups to reduce the parsed tree footprint
1012             # NOTE: groups are now deprecated
1013             #
1014             }
1015              
1016             #########
1017             # Attribute processing for tags in blocks
1018             #
1019             my $tmp;
1020             for my $tag (@parts) {
1021             my $opts = $attr->{$tag}||[];
1022              
1023             for my $a (@{$opts}) {
1024             ($tmp) = $blk =~ m{<$tag[^>]*\s+$a="([^"]+?)"}smix;
1025             if(defined $tmp) {
1026             $ref->{"${tag}_$a"} = $tmp;
1027             }
1028             }
1029              
1030             ($tmp) = $blk =~ m{<$tag[^>]*>([^<]+)}smix;
1031             if(defined $tmp) {
1032             $tmp =~ s/^\s+$//smgx;
1033             if(length $tmp) {
1034             $ref->{$tag} = $tmp;
1035             }
1036             }
1037             if($tmp && $DEBUG) {
1038             print {*STDERR} q( )x($depth*2), qq( $tag = $tmp\n); ## no critic (InputOutput::RequireCheckedSyscalls)
1039             }
1040             }
1041              
1042             $self->_parse_twig($dsn, $blk, $ref, $addseginfo);
1043              
1044             push @{$ar_ref}, $ref;
1045             $DEBUG and print {*STDERR} q( )x($depth*2), qq(leaving _parse_branch\n);
1046              
1047             #########
1048             # only perform callbacks if we're at recursion depth zero
1049             #
1050             if($depth == 0 && $self->{'callback'}) {
1051             $DEBUG and print {*STDERR} q( )x($depth*2), qq(executing callback at depth $depth\n);
1052             $ref->{'dsn'} = $dsn;
1053             my $callback = $self->{'callback'};
1054             &{$callback}($ref);
1055             }
1056              
1057             return q();
1058             }
1059              
1060             sub _parse_twig {
1061             my ($self, $dsn, $blk, $ref, $addseginfo) = @_;
1062              
1063             #########
1064             # handle multiples of twig elements here
1065             #
1066             $blk =~ s/$LINKRE/{
1067             $ref->{'link'} ||= [];
1068             push @{$ref->{'link'}}, {
1069             'href' => $1 || $3,
1070             'txt' => $2,
1071             };
1072             q()
1073             }/smegix;
1074             $blk =~ s/$NOTERE/{
1075             $ref->{'note'} ||= [];
1076             push @{$ref->{'note'}}, $1;
1077             q()
1078             }/smegix;
1079              
1080             if($addseginfo && $self->{'currentsegs'}->{$dsn}) {
1081             while(my ($k, $v) = each %{$self->{'currentsegs'}->{$dsn}}) {
1082             $ref->{$k} = $v;
1083             }
1084             }
1085             return;
1086             }
1087              
1088             sub registry {
1089             my ($self, @reg) = @_;
1090              
1091             if((scalar @reg == 1) &&
1092             (ref $reg[0]) &&
1093             (ref$reg[0] eq 'ARRAY')) {
1094             push @{$self->{'registry'}}, @{$reg[0]};
1095             } else {
1096             push @{$self->{'registry'}}, @reg;
1097             }
1098             return $self->{'registry'};
1099             }
1100              
1101             sub registry_sources {
1102             my ($self, $filters, $flush) = @_;
1103              
1104             $filters ||= {};
1105             my $category = $filters->{'category'} || [];
1106             my $capability = $filters->{'capability'} || $filters->{'capabilities'} || [];
1107              
1108             if(!ref $category) {
1109             $category = [$category];
1110             }
1111              
1112             if(!ref $capability) {
1113             $capability = [$capability];
1114             }
1115              
1116             $flush and $self->{'_registry_sources'} = [];
1117              
1118             #########
1119             # Populate the list of sources if this is the first call or we're flushing
1120             #
1121             if (scalar @{$self->{'_registry_sources'}} == 0) {
1122             $self->_fetch_registry_sources() or return [];
1123             }
1124              
1125             #########
1126             # Jump out if there's no filtering to be done
1127             #
1128             if(!scalar keys %{$filters}) {
1129             return $self->{'_registry_sources'};
1130             }
1131              
1132             my $sources = $self->{'_registry_sources'};
1133              
1134             #########
1135             # Apply capability filter
1136             #
1137             if((ref $capability eq 'ARRAY') &&
1138             (scalar @{$capability})) {
1139             my $str = join q(|), @{$capability};
1140             my $match = qr/$str/smx;
1141             $sources = [grep { $self->_filter_capability($_, $match) } @{$sources}];
1142             }
1143              
1144             #########
1145             # Apply coordinatesystem/category filter
1146             #
1147             if((ref $category eq 'ARRAY') &&
1148             (scalar @{$category})) {
1149             $sources = [grep { $self->_filter_category($_, $category) } @{$sources}];
1150             }
1151              
1152             return $sources;
1153             }
1154              
1155             sub _fetch_registry_sources {
1156             my $self = shift;
1157             my $reg_urls = $self->registry();
1158              
1159             if (!scalar @{ $reg_urls }) {
1160             return;
1161             }
1162              
1163             my $old_dsns = $self->dsn();
1164             my $old_statuses = $self->{'statuscodes'};
1165              
1166             $self->dsn($reg_urls);
1167              
1168             #########
1169             # Run the DAS sources command
1170             #
1171             my $sources_ref = $self->sources();
1172             my $statuses = $self->{'statuscodes'};
1173              
1174             $self->dsn($old_dsns);
1175             $self->{'statuscodes'} = $old_statuses;
1176              
1177             for my $url (keys %{ $sources_ref || {} }) {
1178             my $status = $statuses->{$url} || 'Unknown status';
1179             if ($status !~ m/^200/mxs) {
1180             carp "Error fetching sources from '$url' : $status";
1181             next;
1182             }
1183              
1184             my $ref = $sources_ref->{$url} || [];
1185              
1186             #########
1187             # Some basic checks
1188             #
1189             (ref $ref eq 'ARRAY') || return;
1190             $ref = $ref->[0] || {};
1191             (ref $ref eq 'HASH') || return;
1192             $ref = $ref->{'source'} || [];
1193             (ref $ref eq 'ARRAY') || return;
1194              
1195             #########
1196             # The sources command has sources (really groups of sources) and
1197             # versions (really individual sources). For compatibility with the
1198             # old SOAP way of doing things, we must:
1199             # 1. throw away this source grouping semantic
1200             # 2. convert the hash format to the old style
1201             #
1202             for my $sourcegroup (@{ $ref }) {
1203             $self->_fetch_registry_sources_sourcegroup($sourcegroup);
1204             }
1205             }
1206              
1207             return 1;
1208             }
1209              
1210             sub _fetch_registry_sources_sourcegroup {
1211             my ($self, $sourcegroup) = @_;
1212             my $versions = $sourcegroup->{'version'} || [];
1213             (ref $versions eq 'ARRAY') || next;
1214              
1215             for my $source (@{ $versions }) {
1216             my $caps = $source->{'capability'} || [];
1217             my $dsn;
1218             my $object = {
1219             capabilities => [],
1220             coordinateSystem => [],
1221             description => $sourcegroup->{source_description},
1222             id => $source->{version_uri},
1223             };
1224              
1225             #########
1226             # Some sources have 'more info' URLs
1227             #
1228             if ( my $doc_href = $sourcegroup->{source_doc_href} ) {
1229             $object->{helperurl} = $doc_href;
1230             }
1231              
1232             #########
1233             # Add the capabilties
1234             #
1235             for my $cap (@{ $caps }) {
1236             #########
1237             # Extract the DAS URL from one of the capabilities
1238             # NOTE: in DAS 1 we assume all capability query URLs for one
1239             # source are the same. Anything else would need the data
1240             # model to be redesigned.
1241             #
1242             if (!$dsn) {
1243             $dsn = $cap->{'capability_query_uri'} || q();
1244             ($dsn) = $dsn =~ m{(.+/das\d?/[^/]+)}mxs;
1245             $object->{'url'} = $dsn;
1246             }
1247              
1248             my $cap_type = $cap->{'capability_type'} || q();
1249             ($cap_type) = $cap_type =~ m/das\d:(.+)/mxs;
1250             $cap_type || next;
1251              
1252             push @{ $object->{'capabilities'} }, $cap_type;
1253             }
1254              
1255             #########
1256             # If none of the capabilities have query URLs, we can't query them!
1257             #
1258             $object->{'url'} || next;
1259              
1260             #########
1261             # Add the coordinates
1262             #
1263             my $coords = $source->{'coordinates'} || [];
1264              
1265             for my $coord (@{ $coords }) {
1266             #########
1267             # All coordinates have a name and category
1268             #
1269             my $coord_ob = {
1270             name => $coord->{coordinates_authority},
1271             category => $coord->{coordinates_source},
1272             };
1273              
1274             #########
1275             # Some coordinates have a version
1276             #
1277             if ( my $version = $coord->{'coordinates_version'} ) {
1278             $coord_ob->{'version'} = $version;
1279             }
1280              
1281             #########
1282             # Some coordinates have a species (taxonomy ID and name)
1283             #
1284             if ( my $taxid = $coord->{'coordinates_taxid'} ) {
1285             $coord_ob->{'NCBITaxId'} = $taxid;
1286              
1287             my $desc = $coord->{'coordinates'};
1288             my ($species) = $desc =~ m/([^,]+)$/mxs;
1289              
1290             $coord_ob->{'organismName'} = $species;
1291             }
1292              
1293             #########
1294             # Add the coordinate system
1295             #
1296             push @{ $object->{'coordinateSystem'} }, $coord_ob;
1297             }
1298              
1299             #########
1300             # Add the actual source object
1301             #
1302             push @{ $self->{'_registry_sources'} }, $object;
1303             }
1304             return 1;
1305             }
1306              
1307             sub _filter_capability {
1308             my ($self, $src, $match) = @_;
1309             for my $scap (@{$src->{'capabilities'}}) {
1310             if($scap =~ $match) {
1311             return 1;
1312             }
1313             }
1314             return 0;
1315             };
1316              
1317             sub _filter_category {
1318             my ($self, $src, $match) = @_;
1319             for my $scoord (@{$src->{'coordinateSystem'}}) {
1320             for my $m (@{$match}) {
1321             if ($m =~ m/,/mxs) {
1322             # regex REQUIRES "authority,type", and handles optional version (with proper underscore handling) and species
1323             my ($auth, $ver, $cat, $org) = $m =~ m/^ (.+?) (?:_([^_,]+))? ,([^,]+) (?:,(.+))? /mxs;
1324             if (lc $cat eq lc $scoord->{'category'} &&
1325             $auth eq $scoord->{'name'} &&
1326             (!$ver || lc $ver eq lc $scoord->{'version'}) &&
1327             (!$org || lc $org eq lc $scoord->{'organismName'})) {
1328             return 1;
1329             }
1330             } else {
1331             return 1 if(lc $scoord->{'category'} eq lc $m);
1332             }
1333             }
1334             }
1335             return 0;
1336             }
1337              
1338             1;
1339             __END__