File Coverage

blib/lib/SeeAlso/Server.pm
Criterion Covered Total %
statement 149 231 64.5
branch 75 146 51.3
condition 28 49 57.1
subroutine 18 25 72.0
pod 10 10 100.0
total 280 461 60.7


line stmt bran cond sub pod time code
1 6     6   24961 use strict;
  6         16  
  6         251  
2 6     6   34 use warnings;
  6         14  
  6         342  
3             package SeeAlso::Server;
4             {
5             $SeeAlso::Server::VERSION = '0.71';
6             }
7             #ABSTRACT: SeeAlso Linkserver Protocol Server
8              
9 6     6   7338 use utf8;
  6         74  
  6         42  
10              
11 6     6   221 use Carp qw(croak);
  6         12  
  6         387  
12 6     6   74247 use CGI qw(-oldstyle_urls 3.0);
  6         118213  
  6         52  
13              
14             Carp::carp(__PACKAGE__." is deprecated - please use Plack::App::SeeAlso instead!");
15              
16 6     6   1643 use SeeAlso::Identifier;
  6         17  
  6         141  
17 6     6   1447 use SeeAlso::Response;
  6         44  
  6         163  
18 6     6   1648 use SeeAlso::Source;
  6         17  
  6         293  
19              
20 6     6   51 use base 'Exporter';
  6         15  
  6         22089  
21             our @EXPORT_OK = qw(xmlencode);
22              
23              
24             sub new {
25 11     11 1 11359 my ($class, %params) = @_;
26              
27 11         24 my $logger = $params{logger};
28              
29 11         20 my $cgi = $params{cgi};
30 11 50 66     72 croak('Parameter cgi must be a CGI object!')
31             if defined $cgi and not UNIVERSAL::isa($cgi, 'CGI');
32              
33 11         45 SeeAlso::Source::expand_from_config( \%params, 'Server' );
34              
35 11   100     359 my $self = bless {
      50        
      50        
      50        
      100        
36             cgi => ($cgi || undef),
37             logger => $logger,
38             xslt => ($params{xslt} || undef),
39             clientbase => ($params{clientbase} || undef),
40             debug => ($params{debug} || 0),
41             formats => { 'seealso' => { type => 'text/javascript' } },
42             errors => [],
43             idtype => ($params{idtype} || 'SeeAlso::Identifier'),
44             }, $class;
45              
46             ## no critic
47 11         682 eval "require " . $self->{idtype};
48 11 50       57 croak $@ if $@;
49 11 50       71 croak($self->{idtype} . ' is not a SeeAlso::Identifier')
50             unless UNIVERSAL::isa( $self->{idtype}, 'SeeAlso::Identifier' );
51             ## use critic
52              
53 11 100       37 $self->setExpires($params{expires}) if $params{expires};
54              
55 11 100       29 if ($params{formats}) {
56 2         4 my %formats = %{$params{formats}};
  2         10  
57 2         6 foreach my $name (keys %formats) {
58 2 50 33     17 next if $name eq 'seealso' or $name eq 'debug';
59 2         4 my $format = $formats{$name};
60 2         3 my $value = $formats{$name};
61 2 100       10 if (not $format) {
    50          
62 1         5 $self->{formats}{$name} = 0;
63             } elsif (ref($format) eq 'HASH') {
64             # TODO: enable default format handlers with
65 1 50       4 next unless defined $format->{type};
66 1 50 33     10 next unless ref($format->{filter}) eq 'CODE' or
67             ref($format->{method}) eq 'CODE';
68 1         12 $self->{formats}{$name} = {
69             "type" => $format->{type},
70             "docs" => $format->{docs},
71             "method" => $format->{method},
72             "filter" => $format->{filter},
73             };
74             } else {
75             # enable default format handlers for known formats
76 0 0       0 if ($name eq 'rdfjson') {
    0          
    0          
    0          
77             $self->{formats}{'rdfjson'} = {
78             type => "application/rdf+json",
79 0     0   0 filter => sub { return $_[0]->toRDFJSON; },
80 0         0 };
81             } elsif ($name eq 'n3') {
82             $self->{formats}{'n3'} = {
83             type => "text/n3",
84 0     0   0 filter => sub { return $_[0]->toN3; },
85 0         0 };
86             # } elsif ($name eq 'rdf') {
87             #$self->{formats}{'rdf'} = {
88             # type => "application/rdf+xml",
89             # filter => sub { return $_[0]->toRDFXML; },
90             #};
91             } elsif ($name eq 'csv') {
92             $self->{formats}{'csv'} = {
93             type => "text/csv",
94 0     0   0 filter => sub { return $_[0]->toCSV; },
95 0         0 };
96             } elsif ($name eq 'redirect') {
97             $self->{formats}{'redirect'} = {
98             type => "text/html",
99             raw => 1, # includes HTTP headers
100 0     0   0 filter => sub { return $_[0]->toRedirect($value); },
101 0         0 };
102             }
103             # TODO: ttl : (text/turtle)
104             }
105             }
106             }
107              
108             # enable by default if not disabled
109 11 100       38 if ( not defined $self->{formats}{opensearchdescription} ) {
110 10         43 $self->{formats}{"opensearchdescription"} = {
111             type=>"application/opensearchdescription+xml",
112             docs=>"http://www.opensearch.org/Specifications/OpenSearch/1.1/Draft_3#OpenSearch_description_document"
113             };
114             }
115              
116              
117 11 50       33 $self->logger($params{logger}) if defined $params{logger};
118              
119 11         53 return $self;
120             }
121              
122              
123             sub query {
124 20     20 1 5809 my ($self, $source, $identifier, $format, $callback) = @_;
125 20         30 my $http = "";
126              
127 20 100       69 if (ref($source) eq "CODE") {
128 7         35 $source = new SeeAlso::Source( $source );
129             }
130 20 50 33     142 croak('First parameter must be a SeeAlso::Source or code reference!')
131             unless defined $source and UNIVERSAL::isa($source, 'SeeAlso::Source');
132              
133 20 100       160 if ( ref($identifier) eq 'CODE' ) {
    50          
    50          
134 3         9 $identifier = &$identifier( $self->param('id') );
135             } elsif (UNIVERSAL::isa( $identifier,'SeeAlso::Identifier::Factory' )) {
136 0         0 $identifier = $identifier->create( $self->param('id') );
137             } elsif (not defined $identifier) {
138 0         0 $identifier = $self->param('id');
139             }
140              
141             ## no critic
142 20 100       166 if ( not UNIVERSAL::isa( $identifier, 'SeeAlso::Identifier' ) ) {
143 7         16 my $class = $self->{idtype};
144 7         514 $identifier = eval "new $class(\$identifier)"; # TODO: what if this fails?
145             }
146             ## use critic
147              
148 20 100       64 $format = $self->param('format') unless defined $format;
149 20 50       179 $format = "" unless defined $format;
150 20 100       68 $callback = $self->param('callback') unless defined $callback;
151 20 100       459 $callback = "" unless defined $callback;
152              
153             # If everything is ok up to here, we should definitely return some valid stuff
154 20 50 33     89 $format = "seealso" if ( $format eq "debug" && $self->{debug} == -1 );
155 20 50 66     100 $format = "debug" if ( $format eq "seealso" && $self->{debug} == 1 );
156              
157 20 50       45 if ($format eq 'opensearchdescription') {
158 0         0 $http = $self->openSearchDescription( $source );
159 0 0       0 if ($http) {
160 0         0 $http = CGI::header( -status => 200, -type => 'application/opensearchdescription+xml; charset: utf-8' ) . $http;
161 0         0 return $http;
162             }
163             }
164              
165 20         44 $self->{errors} = []; # clean error list
166 20         34 my $response;
167 20         26 my $status = 200;
168              
169 20 100 66     81 if ( not $identifier ) {
    100 100        
      66        
170 3         8 $self->errors( "invalid identifier" );
171 3         12 $response = SeeAlso::Response->new;
172             } elsif ($format eq "seealso" or $format eq "debug" or !$self->{formats}{$format}
173             or $self->{formats}{$format}->{filter} ) {
174 16         29 eval {
175             local $SIG{'__WARN__'} = sub {
176 1     1   5 $self->errors(shift);
177 16         111 };
178 16         64 $response = $source->query( $identifier );
179             };
180 16 100       39 if ($@) {
181 2         9 $self->errors( $@ );
182 2         3 undef $response;
183             } else {
184 14 50 33     94 if (defined $response && !UNIVERSAL::isa($response, 'SeeAlso::Response')) {
185 0         0 $self->errors( ref($source) . "->query must return a SeeAlso::Response object but it did return '" . ref($response) . "'");
186 0         0 undef $response;
187             }
188             }
189              
190 16 100       42 $response = SeeAlso::Response->new() unless defined $response;
191              
192 16 100 100     73 if ($callback && !($callback =~ /^[a-zA-Z0-9\._\[\]]+$/)) {
193 1         4 $self->errors( "Invalid callback name specified" );
194 1         2 undef $callback;
195 1         2 $status = 400;
196             }
197             } else {
198 1         5 $response = SeeAlso::Response->new( $identifier );
199             }
200              
201              
202 20 50       68 if ( $self->{logger} ) {
203 0         0 my $service = $source->description( "ShortName" );
204 0         0 eval {
205 0 0       0 $self->{logger}->log( $self->{cgi}, $response, $service )
206             || $self->errors("Logging failed");
207             };
208 0 0       0 $self->errors( $@ ) if $@;
209             }
210              
211 20 100       48 if ( $format eq "seealso" ) {
    50          
212 16         75 my %headers = (-status => $status, -type => 'text/javascript; charset: utf-8');
213 16 100       43 $headers{"-expires"} = $self->{expires} if ($self->{expires});
214 16         448 $http .= CGI::header( %headers );
215 16         6458 $http .= $response->toJSON($callback);
216             } elsif ( $format eq "debug") {
217 0         0 $http .= CGI::header( -status => $status, -type => 'text/javascript; charset: utf-8' );
218 0         0 $http .= "/*\n";
219            
220             # TODO
221             # use Class::ISA; # deprecated
222             # my %vars = ( Server => $self, Source => $source, Identifier => $identifier, Response => $response );
223             # foreach my $var (keys %vars) {
224             # $http .= "$var is a " .
225             # join(", ", map { $_ . " " . $_->VERSION; }
226             # Class::ISA::self_and_super_path(ref($vars{$var})))
227             # . "\n"
228             # }
229 0         0 $http .= "\n";
230 0         0 $http .= "HTTP response status code is $status\n";
231 0 0       0 $http .= "\nInternally the following errors occured:\n- "
232             . join("\n- ", $self->errors) . "\n" if $self->errors;
233 0         0 $http .= "*/\n";
234 0         0 $http .= $response->toJSON($callback) . "\n";
235             } else { # other unAPI formats
236             # TODO is this properly logged?
237             # TODO: put 'seealso' as format method in the array
238 4         10 my $f = $self->{formats}{$format};
239 4 100       12 if ($f) {
240 1 50       6 if ($f->{filter}) {
241 0         0 $http = $f->{filter}($response); # TODO: what if this fails?!
242             } else {
243 1         5 $http = $f->{method}($identifier); # TODO: what if this fails?!
244             }
245 1 50       5 if (!$f->{raw}) { # TODO: Autodetect headers if already in HTTP
246 1         4 my $type = $f->{type} . "; charset: utf-8";
247 1         31 my $header = CGI::header( -status => $status, -type => $type );
248 1         346 $http = $header . $http;
249             }
250             } else { # unknown format or not defined format
251 3         10 $http = $self->listFormats($response);
252             }
253             }
254 20         146 return $http;
255             }
256              
257              
258             sub param {
259 27     27 1 42 my ($self, $name) = @_;
260 27 100       108 if ( defined $self->{cgi} ) {
261 23         28 1;
262 23         83 return $self->{cgi}->param($name);
263             }
264              
265 4 50       25 return defined $self->{cgi} ? $self->{cgi}->param($name) : CGI::param($name);
266              
267 0         0 return CGI::param($name);
268             }
269              
270              
271             sub logger {
272 0     0 1 0 my $self = shift;
273 0         0 my $logger = shift;
274 0 0       0 return $self->{logger} unless defined $logger;
275 0 0       0 if (!UNIVERSAL::isa($logger, 'SeeAlso::Logger')) {
276 0         0 $logger = SeeAlso::Logger->new($logger);
277             }
278 0         0 $self->{logger} = $logger;
279             }
280              
281              
282             sub setExpires {
283 1     1 1 3 my ($self, $expires) = @_;
284 1         4 $self->{expires} = $expires;
285             }
286              
287              
288             sub listFormats {
289 7     7 1 249 my ($self, $response) = @_;
290              
291 7         10 my $status = 200;
292 7         26 my $id = $response->query();
293 7 100       23 if ($response->query() ne "") {
294 4 100       18 $status = $response->size ? 300 : 404;
295             }
296              
297 7         191 my $headers = CGI::header( -status => $status, -type => 'application/xml; charset: utf-8' );
298 7         4024 $headers .= '' . "\n";
299              
300 7 50       27 if ($self->{xslt}) {
301 0         0 $headers .= "{xslt}) . "\"?>\n";
302 0         0 $headers .= "baseURL) . "?>\n";
303             }
304 7 50       19 if ($self->{clientbase}) {
305 0         0 $headers .= "{clientbase}) . "?>\n";
306             }
307              
308 7         21 return _unapiListFormats( $self->{formats}, $id, $headers );
309             }
310              
311             # $formats: hash reference
312             # $id : scalar (optional)
313             # $headers : scalar (optional, use undef to disable)
314             sub _unapiListFormats { # TODO: move this to HTTP::unAPI or such
315 7     7   15 my ($formats, $id, $headers) = @_;
316              
317 7 50       20 $headers = '' unless defined $headers;
318            
319 7         8 my @xml;
320 7 100       25 if ($id ne "") {
321 4         12 push @xml, '';
322             } else {
323 3         7 push @xml, '';
324             }
325              
326 7         44 foreach my $name (sort({$b cmp $a} keys(%$formats))) {
  7         26  
327 14         20 my $format = $formats->{$name};
328 14 100 66     73 if ( $format && $name ne 'debug' ) {
329 13         30 my $fstr = "{type}) . "\"";
330 13 100       45 $fstr .= " docs=\"" . xmlencode($format->{docs}) . "\"" if defined $format->{docs};
331 13         47 push @xml, $fstr . " />";
332             }
333             }
334              
335 7         14 push @xml, '';
336              
337 7         69 return $headers . join("\n", @xml) . "\n";
338             }
339              
340              
341             sub errors {
342 11     11 1 28 my $self = shift;
343 11         20 my $message = shift;
344 11 100       30 if ( defined $message ) {
345 7         15 chomp $message;
346 7         10 push @{ $self->{errors} }, $message;
  7         20  
347             }
348 11         15 return @{ $self->{errors} };
  11         51  
349             }
350              
351              
352             sub openSearchDescription {
353 0     0 1 0 my ($self, $source) = @_;
354              
355 0         0 my $baseURL = $self->baseURL;
356              
357 0 0 0     0 return unless $source and UNIVERSAL::isa( $source, "SeeAlso::Source" );
358 0         0 my %descr = %{ $source->description };
  0         0  
359              
360 0         0 my @xml = '';
361 0         0 push @xml, '';
362              
363 0         0 my $shortName = $descr{"ShortName"}; # TODO: shorten to 16 chars maximum
364 0 0       0 push @xml, " " . xmlencode( $shortName ) . ""
365             if defined $shortName;
366              
367 0         0 my $longName = $descr{"LongName"}; # TODO: shorten to 48 chars maximum
368 0 0       0 push @xml, " " . xmlencode( $longName ) . ""
369             if defined $longName;
370              
371 0         0 my $description = $descr{"Description"}; # TODO: shorten to 1024 chars maximum
372 0 0       0 push @xml, " " . xmlencode( $description ) . ""
373             if defined $description;
374              
375 0 0       0 $baseURL = $descr{"BaseURL"} # overwrites standard
376             if defined $descr{"BaseURL"};
377              
378 0         0 my $modified = $descr{"DateModified"};
379 0 0       0 push @xml, " " . xmlencode( $modified ) . ""
380             if defined $modified;
381              
382 0         0 my $src = $descr{"Source"};
383 0 0       0 push @xml, " " . xmlencode( $src ) . ""
384             if defined $src;
385              
386 0 0       0 if ($descr{"Examples"}) { # TODO: add more parameters
387 0         0 foreach my $example ( @{ $descr{"Examples"} } ) {
  0         0  
388 0         0 my $id = $example->{id};
389 0         0 my $args = "searchTerms=\"" . xmlencode($id) . "\"";
390 0         0 my $response = $example->{response};
391 0 0       0 if (defined $response) {
392 0         0 $args .= " seealso:response=\"" . xmlencode($response) . "\"";
393             }
394 0         0 push @xml, " ";
395             }
396             }
397            
398 0 0       0 my $template = $baseURL . (($baseURL =~ /\?/) ? '&' : '?')
399             . "id={searchTerms}&format=seealso&callback={callback}";
400 0         0 push @xml, " ";
401 0         0 push @xml, "";
402              
403 0         0 return join("\n", @xml);
404             }
405              
406              
407             sub baseURL {
408 0     0 1 0 my $self = shift;
409 0         0 my $cgi = $self->{cgi};
410              
411 0 0       0 my $url = (defined $cgi ? $cgi->url : CGI::url());
412              
413             # remove id, format, and callback parameter
414 0 0       0 my $q = "&" . (defined $cgi ? $cgi->query_string() : CGI::query_string());
415 0         0 $q =~ s/&(id|format|callback)=[^&]*//g;
416 0         0 $q =~ s/^&//;
417              
418 0 0       0 $url .= "?$q" if $q;
419 0         0 return $url;
420             }
421              
422              
423             sub xmlencode {
424 36     36 1 53 my $data = shift;
425 36 50       101 if ($data =~ /[\&\<\>"]/) {
426 0         0 $data =~ s/\&/\&\;/g;
427 0         0 $data =~ s/\
428 0         0 $data =~ s/\>/\>\;/g;
429 0         0 $data =~ s/"/\"\;/g;
430             }
431 36         114 return $data;
432             }
433              
434             1;
435              
436             __END__