File Coverage

blib/lib/Keystone/Resolver/OpenURL.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             # $Id: OpenURL.pm,v 1.28 2008-04-30 16:39:00 mike Exp $
2              
3             package Keystone::Resolver::OpenURL;
4              
5 4     4   19 use strict;
  4         7  
  4         120  
6 4     4   18 use warnings;
  4         4  
  4         80  
7 4     4   17 use Encode;
  4         6  
  4         335  
8 4     4   20 use URI::Escape qw(uri_escape_utf8);
  4         13  
  4         161  
9 4     4   4956 use XML::LibXSLT;
  0            
  0            
10             use Scalar::Util;
11             use Keystone::Resolver::Result;
12              
13              
14             =head1 NAME
15              
16             Keystone::Resolver::OpenURL - an OpenURL that can be resolved
17              
18             =head1 SYNOPSIS
19              
20             %args = (genre => "article",
21             issn => "0141-1926",
22             volume => 29,
23             issue => 4,
24             spage => 471);
25             $openURL = $resolver->openURL(\%args, "http://my.domain.com/resolve");
26             @results = $openURL->resolve_to_results();
27             $xml = $openURL->resolve_to_xml();
28             $html = $openURL->resolve_to_html($stylesheetName);
29             ($type, $content) = $openURL->resolve();
30              
31             print $openURL->resolve();
32              
33             =head1 DESCRIPTION
34              
35             This class represents an OpenURL, which may be resolved.
36              
37             =head1 METHODS
38              
39             =cut
40              
41              
42             =head2 new()
43              
44             $openURL = new Keystone::Resolver::OpenURL($resolver, \%args,
45             "http://my.domain.com/resolve");
46             # Or the more usual shorthand:
47             $openURL = $resolver->openURL(\%args, "http://my.domain.com/resolve");
48              
49             Creates a new OpenURL object, which can subsequently be resolved. The
50             first argument is a reference to a hash of the arguments making up the
51             OpenURL data packet that specifies the ContextObject. The second is
52             the base URL of the resolver, which is used for generating equivalent
53             URLs as required. The third is the referer URL, which may be used for
54             some primitive authentication schemes.
55              
56             =cut
57              
58             sub new {
59             my $class = shift();
60             my($resolver, $argsref, $base, $referer) = @_;
61              
62             my $this = bless {
63             resolver => $resolver,
64             base => $base,
65             referer => $referer, # not yet used, but needed for authentication
66             serial => undef, # to be filled in by _serial() if required
67             results => undef, # to be filled in by resolve_to_results()
68             }, $class;
69              
70             Scalar::Util::weaken($this->{resolver});
71              
72             ### It may be a mistake that we have separate OpenURL and
73             # ContextObject classes, as there is always a one-to-one
74             # correspondance between them.
75             $this->{co} = new Keystone::Resolver::ContextObject($resolver,
76             $this, $argsref);
77              
78             $this->log(Keystone::Resolver::LogLevel::LIFECYCLE, "new OpenURL $this");
79             return $this;
80             }
81              
82              
83             sub DESTROY {
84             my $this = shift();
85             Keystone::Resolver::static_log(Keystone::Resolver::LogLevel::LIFECYCLE,
86             "dead OpenURL $this");
87             }
88              
89              
90             =head2 newFromCGI()
91              
92             $openURL = newFromCGI Keystone::Resolver::OpenURL($resolver, $cgi,
93             $ENV{HTTP_REFERER});
94             $openURL = newFromCGI Keystone::Resolver::OpenURL($resolver, $cgi,
95             $ENV{HTTP_REFERER}, { xml => 1, loglevel => 7 });
96              
97             This convenience method creates an OpenURL object from a set of CGI
98             parameters, for the common case that the transport is HTTP-based. it
99             behaves the same as the general constructor, C, except that
100             that a C object is passed in place of the C<$argsref> and
101             C<$baseURL> arguments.
102              
103             Additionally, a set of options may be passed in: unless overridden by
104             options in the CGI parameters, these are applied to the C<$resovler>.
105             Parameters in C<$cgi> whose keys are prefixed with C are
106             interpreted as resolver options, like this:
107              
108             opt_loglevel=7&opt_logprefix=ERROR
109              
110             All other keys in the CGI object are assumed to be part of the OpenURL
111             context object.
112              
113             (### The option handling is arguably a mistake: the options should
114             apply to the OpenURL object, not the resolver that it uses -- but at
115             present, OpenURL objects do not have their own options at all.)
116              
117             =cut
118              
119             sub newFromCGI {
120             my $class = shift();
121             # my($cgi, $referer, $optsref, $resolver) = @_;
122             my($resolver, $cgi, $referer, $optsref) = @_;
123             die "no resolver defined in newFromCGI()"
124             if !defined $resolver;
125              
126             my %args;
127             my %opts = defined $optsref ? %$optsref : ();
128              
129             # Default options set from the environment: the KR_OPTIONS
130             # environment variable is of the form: loglevel=32,xml=1
131             my $optstr = $ENV{KR_OPTIONS};
132             if (defined $optstr) {
133             foreach my $pair (split /,/, $optstr) {
134             my($key, $val) = ($pair =~ /(.*)=(.*)/);
135             $opts{$key} = $val;
136             }
137             }
138              
139             foreach my $key ($cgi->param()) {
140             if ($key =~ /^opt_/) {
141             my @val = $cgi->param($key);
142             $key =~ s/^opt_//;
143             die "Oops. Multiple values for option '$key'" if @val > 1;
144             $opts{$key} = $val[0];
145             #print STDERR "set option($key) -> ", $val[0], "\n";
146             } else {
147             push @{ $args{$key} }, ($cgi->param($key));
148             #print STDERR "$key = ", join(", ",@{$args{$key}}), "\n";
149             }
150             }
151              
152             foreach my $key (keys %opts) {
153             $resolver->option($key, $opts{$key});
154             }
155              
156             my $baseURL = $opts{baseURL} || $cgi->url();
157             return $class->new($resolver, \%args, $baseURL, $referer);
158             }
159              
160              
161             sub resolver { return shift()->{resolver} }
162             sub base { return shift()->{base} }
163             sub co { return shift()->{co} }
164              
165             # Delegations -- not so simple now that the resolver reference is weak!
166             sub option {
167             my $this = shift();
168             my($key, $value) = @_;
169             my $resolver = $this->{resolver};
170             if (defined $resolver) {
171             return $resolver->option(@_);
172             } else {
173             warn("OpenURL::option('$key', " .
174             (defined $value ? "'$value'" : "undef") .
175             ") on weakened {resolver}, returning undef");
176             return undef;
177             }
178             }
179              
180             sub log {
181             my $this = shift();
182             my $resolver = $this->{resolver};
183             if (defined $resolver) {
184             return $resolver->log(@_);
185             } else {
186             warn "weakened {resolver} reference has become undefined: logging @_";
187             }
188             }
189             sub descriptor { my $this = shift(); return $this->co()->descriptor(@_) }
190             sub rft { my $this = shift(); return $this->descriptor("rft")->metadata1(@_) }
191              
192             # Special delegation: knows database name from OpenURL argument
193             sub db {
194             my $this = shift();
195             use Carp; confess "resolver link is undefined" if !defined $this->{resolver};
196             return $this->resolver()->db(@_ ? @_ : $this->option("db"));
197             }
198              
199              
200             =head2 die(), warn()
201              
202             $openURL->die("no service available: ", $errmsg, "(", $details, ")");
203             $openURL->warn("multiple genres with same ID");
204              
205             C reports failure to resolve this OpenURL. A nicely formatted
206             message may be displayed for the user, a message may be generated in a
207             log file, an email may be sent to the administrator or some
208             combination of these actions may be taken. In any case, the arguments
209             are concatenated to form a string used in these messages.
210              
211             C is the same, except that it indicates a non-fatal condition.
212              
213             =cut
214              
215             sub die {
216             my $this = shift();
217              
218             ### We could choose what to have die() and warn() do based on
219             # configuration options. For now, they just fall back to Perl's
220             # built-in die() and warn(), but we could, for example, format a
221             # nice message for the user.
222              
223             # I can't persuade Carp to do what I want here - Carp::CarpLevel=1
224             # skips right over the stack-frame I want - so I am crudely
225             # locating the relevant caller frame by hand. The approach is
226             # that the immediate caller will usually be a trivial delegate if
227             # it's from outside this package, so in that case we want the next
228             # one down.
229             my($package, $filename, $line) = caller(0);
230             ($package, $filename, $line) = caller(1)
231             if $package ne "Keystone::Resolver::OpenURL";
232              
233             CORE::die "*** fatal: " . join("", @_) . " at $filename line $line.\n";
234             }
235              
236             sub warn {
237             my $this = shift();
238              
239             return if $this->option("nowarn");
240              
241             # See comments in die() above
242             my($package, $filename, $line) = caller(0);
243             ($package, $filename, $line) = caller(1)
244             if $package ne "Keystone::Resolver::OpenURL";
245              
246             $this->log(Keystone::Resolver::LogLevel::WARNING,
247             join("", @_) . " at $filename line $line");
248             }
249              
250              
251             =head2 arg1()
252              
253             $scalar = $openurl->arg1($hashref, $key, $delete, $allowUndef);
254              
255             This simple utility method extracts the first element (i.e. element 0)
256             from the specified element of the specified hash and returns it,
257             throwing an error if that element doesn't exist, isn't an array
258             reference or has no elements, and warning if it has more than one.
259              
260             $openurl->arg1($hashref, "name")
261              
262             is precisely equivalent to
263              
264             $hashref->{name}->[0]
265              
266             except for the extra checking.
267              
268             If the optional third argument, C<$delete> is provided and non-zero,
269             then C<$hashref->[$key]> is deleted as a side-effect.
270              
271             If the optional fourth argument, C<$allowUndef> is provided and
272             non-zero, then no error is raised if C<$hashref->[$key]> is undefined:
273             instead, an undefined value is returned.
274              
275             =cut
276              
277             sub arg1 {
278             my $this = shift();
279             my($hashref, $key, $delete, $allowUndef) = @_;
280              
281             my $arrayref = $hashref->{$key};
282             return undef
283             if !defined $arrayref && $allowUndef;
284             $this->die("element '$key' does not exist")
285             if !defined $arrayref;
286             delete $hashref->{$key}
287             if defined $delete;
288             $this->die("element '$key' ($arrayref) is not an array reference")
289             if ref($arrayref) ne "ARRAY";
290             $this->die("element '$key' has no elements")
291             if @$arrayref == 0;
292             if (@$arrayref > 1) {
293             # When Openly's "OpenURL Referer" Firefox add-on, version
294             # 2.3.5, sees a COinS object in OCLC's WorldCat, the OpenURL
295             # that it generates has two "url_ver=Z39.88-2004" elements.
296             # Since this is an important source of OpenURLs for testing,
297             # we want to allow this without a warning, even though it's
298             # naughty.
299             my @values = @$arrayref;
300             my $val0 = shift @values;
301             $this->warn("element '$key' has multiple conflicting values: ",
302             join(", ", map { "'$_'" } @$arrayref))
303             if grep { $_ ne $val0 } @values;
304             }
305              
306             return $arrayref->[0];
307             }
308              
309              
310             =head2 resolve_to_results(), resolve_to_xml(), resolve_to_html(), resolve()
311              
312             @results = $openURL->resolve_to_results();
313             $xml = $openURL->resolve_to_xml();
314             $html1 = $openURL->resolve_to_html();
315             $html2 = $openURL->resolve_to_html($stylesheetName);
316              
317             The various C methods all resolve a
318             C object into a list of candidate objects
319             that satisfy the link. They differ only in the form in which they
320             return the information.
321              
322             =over 4
323              
324             =item resolve_to_results()
325              
326             Returns an array of zero or more C
327             objects, from which the type and text of results can readily be
328             extracted.
329              
330             =cut
331              
332             sub resolve_to_results {
333             my $this = shift();
334              
335             if (defined $this->{results}) {
336             # Avoid repeated work if two resolve_*() methods are called
337             return @{ $this->{results} };
338             }
339              
340             my($type, $tag) = $this->_single_service();
341             if (defined $type) {
342             # We only want this single service, not all available ones
343             my $service = $this->db()->service_by_type_and_tag($type, $tag);
344             $this->die("no $type service with tag '$tag'") if !defined $service;
345             $this->_add_result($service, 1);
346             goto DONE;
347             }
348              
349             my $rft = $this->descriptor("rft");
350             # This under-the-bonnet pre-test is only for efficiency
351             if ($this->option("loglevel") &
352             Keystone::Resolver::LogLevel::DUMPREFERENT) {
353             use Data::Dumper;
354             $this->log(Keystone::Resolver::LogLevel::DUMPREFERENT, Dumper($rft));
355             }
356              
357             my $errmsg = $this->_resolve_ids($rft);
358             $this->die("resolve_ids: $errmsg")
359             if defined $errmsg;
360             ### What about resolving IDs for the other descriptors?
361              
362             ### Resolve private data indicated by "dat" attribute in each
363             # descriptor, probably in the same way as well-known IDs.
364              
365             my $mformat = $rft->superdata1("val_fmt");
366             if (defined $mformat) {
367             $errmsg = $this->_resolve_metadata($mformat);
368             $this->die("resolve_metadata: $errmsg")
369             if defined $errmsg;
370             } elsif (@{ $this->{results} } == 0) {
371             # This is only a problem if there are no resolved IDs either
372             $this->die("no metadata format specified for referent");
373             }
374              
375             DONE:
376             return @{ $this->{results} };
377             }
378              
379              
380             # The job of this function is just to see whether the ContextObject
381             # specifies that the resolver is to deliver a single service (e.g. a
382             # citation) rather than the usual array of options. If so, we return
383             # that service's type and tag; otherwise an undefined value.
384             #
385             # Unfortunately, there's no standard way to express requesting a
386             # particular service. Clearly this is a property of service-type, so
387             # it belongs in the "svc" descriptor, but the metadata format for
388             # scholarly services defined at
389             # http://www.openurl.info/registry/docs/info:ofi/fmt:kev:mtx:sch_svc
390             # only lets us say yes or no to whether we want services of each of
391             # the obvious types (abstract, citation, fulltext, holdings, ill) and
392             # not to say anything more detailed. An alternative would be to use
393             # svc_id, but the registry at
394             # http://openurl.info/registry/
395             # doesn't seen to define any info-URIs describing service-types. So
396             # it seems we are reduced to using a private identifier, "svc_dat".
397             # At least there is precedent for this in the KEV Guidelines document,
398             # Example 10.6.4 (ServiceType) on page 31:
399             # &svc_dat=addToCart
400             #
401             # What value should we use of svc_dat? To minimise the likelihood of
402             # name-clashes with independent OpenURL 1.0 implementations, we prefix
403             # all our private-data values with "indexdata". Then we use the type
404             # (e.g. "citation"), followed by the tag of the specific service we
405             # want (e.g. a citation style), with all components colon-separated,
406             # like this:
407             # indexdata:citation:endnote
408             #
409             sub _single_service {
410             my $this = shift();
411              
412             my $svc = $this->descriptor("svc");
413             return undef if !defined $svc; # nothing about service-type in CO
414             my $svc_dat = $svc->superdata1("dat");
415             return undef if !defined $svc_dat; # no service-type private data
416             my($prefix, $type, $tag) = ($svc_dat =~ /(.*?):(.*?):(.*)/);
417             return undef if !defined $tag; # unrecognised format;
418             return undef if $prefix ne "indexdata"; # someone else's private data
419             return ($type, $tag);
420             }
421              
422              
423             sub _resolve_ids {
424             my $this = shift();
425             my($d) = @_;
426              
427             my $idrefs = $d->superdata("id");
428             return undef
429             if !defined $idrefs;
430              
431             foreach my $id (@$idrefs) {
432             my $errmsg = $this->_resolve_one_id($d, $id);
433             return $errmsg
434             if defined $errmsg;
435             }
436              
437             return undef;
438             }
439              
440              
441             sub _resolve_one_id {
442             my $this = shift();
443             my($d, $id) = @_;
444              
445             if ($id eq "") {
446             $this->warn("ignoring empty ", $d->name(), " ID");
447             return undef;
448             }
449              
450             # ID should be a URI, for example:
451             # mailto:jane.doe@caltech.edu
452             # info:doi/10.1006/mthe.2000.0239
453             # info:sid/elsevier.com:ScienceDirect
454             my($scheme, $address) = ($id =~ /(.*?):(.*)/);
455             if (!defined $scheme) {
456             $this->warn("ID doesn't seem to be a URI: '$id'");
457             return undef;
458             }
459              
460             eval {
461             require "Keystone/Resolver/plugins/ID/$scheme.pm";
462             }; if ($@) {
463             $this->warn("can't load ID plugin '$scheme': $@");
464             return "ID URI-scheme '$scheme' not supported";
465             }
466              
467             my($uri, $tag, $data, $errmsg, $nonfatal) =
468             "Keystone::Resolver::plugins::ID::$scheme"->data($this, $address);
469             $this->_log_resolve_id($id, $scheme, $uri, $tag, $data, $errmsg, $nonfatal)
470             if $this->option("loglevel") & Keystone::Resolver::LogLevel::RESOLVEID;
471              
472             if (!defined $uri && !defined $data) {
473             return $errmsg if !$nonfatal;
474             $this->_add_error($errmsg) if defined $errmsg;
475             return undef;
476             }
477              
478             if (defined $uri) {
479             # The identifier resolved completely into the URI of the result
480             my $res = new Keystone::Resolver::Result("id", $tag,
481             undef, undef, $uri);
482             $this->log(Keystone::Resolver::LogLevel::MKRESULT, $res->render());
483             push @{ $this->{results} }, $res;
484             }
485              
486             if (defined $data) {
487             # The identifier yielded additional metadata to be used further
488             foreach my $key (keys %$data) {
489             $d->push_metadata($key, $data->{$key});
490             }
491             }
492              
493             return undef;
494             }
495              
496              
497             sub _log_resolve_id {
498             my $this = shift();
499             my($id, $scheme, $uri, $tag, $data, $errmsg, $nonfatal) = @_;
500              
501             my $str = "$id";
502             if (defined $uri) {
503             $str .= ": [$tag] $uri";
504             }
505             if (defined $data) {
506             $str .= ": { " . join(", ", map { "$_ -> \"" . $data->{$_} . "\"" }
507             sort keys %$data) . "}";
508             }
509             if (!defined $uri && !defined $data) {
510             my $non = defined $nonfatal ? "non" : "";
511             $str .= " failed ($non" . "fatal)";
512             if (defined $errmsg) {
513             $str .= ": $errmsg";
514             } else {
515             $str .= " with no error-message";
516             }
517             }
518              
519             $this->log(Keystone::Resolver::LogLevel::RESOLVEID, $str);
520             }
521              
522              
523             sub _resolve_metadata {
524             my $this = shift();
525             my($mformat) = @_;
526              
527             # What does the metadata format actually tell us? We can use
528             # it to guess the genre, but its primary role is to act as a
529             # "namespace identifier" for the metadata elements (aulast,
530             # jtitle, etc.) In theory, we should treat "aulast" in the
531             # "journal" metadata format as a separate and distinct element
532             # from the name-named element in the "book" metadata format. In
533             # practice, that would introduce a lot of extra complexity for
534             # little or no gain.
535              
536             # Gather service types that can resolve items of the required genre
537             my $db = $this->db();
538             my $genre;
539             my $genreTag = $this->rft("genre");
540             if (defined $genreTag) {
541             $genre = $db->genre_by_tag($genreTag);
542             return "unsupported genre '$genreTag' specified"
543             if !defined $genre;
544             } else {
545             $genre = $db->genre_by_mformat($mformat);
546             return "no genre specified, and none defined as default " .
547             "for metadata format '$mformat'"
548             if !defined $genre;
549             }
550              
551             $this->log(Keystone::Resolver::LogLevel::SHOWGENRE,
552             "genre=", $genre->render());
553              
554             # Now we need to determine which service-types to use. Begin by
555             # populating the set with "include" rules that match our data. If
556             # no such rules fired, default to including all service-types
557             # applicable to this genre; finally remove from the set any
558             # service types ruled out by "exclude" rules.
559             my @st;
560             my $strules = $this->_gather_rules("ServiceTypeRule");
561             $this->_process_rules($strules, \@st, 0, "ServiceType");
562             @st = $db->servicetypes_by_genre($genre->id()) if @st == 0;
563             $this->_process_rules($strules, \@st, 1, "ServiceType");
564             return "no service-types for genre '" . $genre->tag() . "'"
565             if @st == 0;
566              
567             my $srules = $this->_gather_rules("ServiceRule");
568             foreach my $st (@st) {
569             my $errmsg = $this->_add_results_for_servicetype($st, $srules);
570             return undef if defined $errmsg && $errmsg eq 0;
571             return $errmsg if defined $errmsg;
572             }
573              
574             return undef;
575             }
576              
577              
578             # Returns a reference to a hash of all rules of the specified class,
579             # indexed by the bipartite string =
580             #
581             sub _gather_rules {
582             my $this = shift();
583             my($class) = @_;
584              
585             my @list = $this->db()->find($class);
586             my %hash;
587             foreach my $rule (@list) {
588             my $fieldname = $rule->fieldname();
589             my $value = $rule->value();
590             $hash{"$fieldname=$value"} = $rule;
591             }
592              
593             return \%hash;
594             }
595              
596              
597             sub _process_rules {
598             my $this = shift();
599             my($ruleset, $stref, $exclude, $class) = @_;
600              
601             CORE::die "_process_rules(class=$class) unknown"
602             if !grep { $class eq $_ } qw(ServiceType Service);
603              
604             my $db = $this->db();
605             foreach my $rule (values %$ruleset) {
606             my $value = $this->_singleDatum($rule->fieldname());
607             if ($rule->deny() == $exclude &&
608             defined $value && $value eq $rule->value()) {
609             my @tags = split /\s+/, $rule->tags();
610             if ($exclude) {
611             my @newst = ();
612             foreach my $st (@$stref) {
613             push @newst, $st if !grep { defined $st->tag() &&
614             $st->tag() eq $_ } @tags;
615             }
616             @$stref = @newst;
617             } elsif ($class eq "ServiceType") {
618             push @$stref, $db->servicetypes_by_tags(@tags);
619             } else {
620             push @$stref, $db->services_by_tags(@tags);
621             }
622             }
623             }
624             }
625              
626              
627             # Performs checks (e.g. authorisation) that are common to all service
628             # types. Returns an error message if something goes wrong, 0 if all
629             # is OK and no more service-types need to be consulted because of an
630             # "include" rule firing, and undef if all is OK and processing should
631             # continue.
632             #
633             sub _add_results_for_servicetype {
634             my $this = shift();
635             my($serviceType, $rules) = @_;
636              
637             my @services;
638             my $gotIncludedServices = 0;
639             $this->_process_rules($rules, \@services, 0, "Service");
640             $gotIncludedServices = 1 if @services > 0;
641             @services = $this->db()->services_by_type($serviceType->id())
642             if @services == 0;
643             $this->_process_rules($rules, \@services, 1, "Service");
644              
645             foreach my $service (@services) {
646             if ($service->disabled()) {
647             $this->log(Keystone::Resolver::LogLevel::CHITCHAT,
648             "skipping disabled service ", $service->render());
649             next;
650             }
651              
652             if ($service->need_auth()) {
653             ### Should determine the user's identity and omit the
654             # services the user has no credentials for.
655             }
656              
657             my $errmsg = $this->_add_result($service);
658             return $errmsg if defined $errmsg;
659             }
660              
661             return $gotIncludedServices ? 0 : undef;
662             }
663              
664              
665             # Checks that are specific some individual service-types (e.g.
666             # coverage of full-text services) are done here and below.
667             # Returns undef if all is OK, an error message otherwise. If optional
668             # third parameter is present and true, this result is the only one
669             # that was asked for, due to a service-type specifier in the Context
670             # Object.
671             #
672             sub _add_result {
673             my $this = shift();
674             my($service, $single) = @_;
675              
676             my($text, $errmsg, $nonfatal, $mimeType) =
677             $this->_make_result($service);
678             if (defined $text) {
679             my $res = new Keystone::Resolver::Result($service->service_type_tag(),
680             $service->tag(),
681             $service->name(),
682             $service->priority(),
683             $text,
684             $mimeType,
685             $single);
686             $this->log(Keystone::Resolver::LogLevel::MKRESULT, $res->render());
687             push @{ $this->{results} }, $res;
688             return undef;
689             } elsif (!defined $errmsg) {
690             # No-op, e.g. repeated failure on the same missing journal record
691             return undef;
692             } elsif ($nonfatal) {
693             $this->_add_error($errmsg);
694             return undef;
695             }
696              
697             # Otherwise it's a hard error
698             return $errmsg;
699             }
700              
701              
702             sub _make_result {
703             my $this = shift();
704             my($service) = @_;
705              
706             my $stype = ($service->service_type_plugin() ||
707             $service->service_type_tag());
708             eval {
709             require "Keystone/Resolver/plugins/ServiceType/$stype.pm";
710             }; if ($@) {
711             $this->warn("can't load service-type plugin '$stype' ",
712             "for service ", $service->name(), ": $@");
713             return (undef, "service-type '$stype' is not supported");
714             }
715              
716             my($text, $errmsg, $nonfatal, $mimeType) =
717             "Keystone::Resolver::plugins::ServiceType::$stype"->handle($this,
718             $service);
719              
720             $this->log(Keystone::Resolver::LogLevel::HANDLE, $service->render(), ": ",
721             (defined $text ?
722             ($text . (defined $mimeType ? " ($mimeType)" : "")) :
723             (!defined $errmsg ? "no-op" :
724             ((defined $nonfatal ? "non-fatal " : "") .
725             "error: ", $errmsg))));
726              
727             return ($text, $errmsg, $nonfatal, $mimeType);
728             }
729              
730              
731             sub _add_error {
732             my $this = shift();
733              
734             push(@{ $this->{results} },
735             new Keystone::Resolver::Result("error", undef, undef, undef,
736             join("", @_)));
737             }
738              
739              
740             # This method is provided for the use of service-type plugins such as
741             # plugins/ServiceType/fulltext.pm. It caches the serial object
742             # required to satisfy an OpenURL, and returns it. It caches the
743             # absence of a suitable serial, to avoid repeated failures. Return
744             # values:
745             # undef Lookup tried, and failed; in this case, an error
746             # message is also returned for display to the user.
747             # 0 Previous lookup failed so didn't try again
748             # Lookup suceeded *or* cached value used from prior success
749             #
750             sub _serial {
751             my $this = shift();
752              
753             my $obj = $this->{serial};
754             return $obj
755             if defined $obj;
756              
757             my $issn = $this->rft("issn");
758             my $jtitle = $this->rft("jtitle");
759             if (!defined $issn && !defined $jtitle) {
760             $this->{serial} = 0;
761             return (undef, "no journal information provided");
762             }
763              
764             $obj = $this->db()->serial($issn, $jtitle);
765             if (!defined $obj) {
766             my $errmsg = ("the resource database doesn't cover " .
767             (defined $issn ? "ISSN $issn" : "") .
768             (defined $issn && defined $jtitle ? ", " : "").
769             (defined $jtitle ? "journal title $jtitle" : ""));
770             $this->warn($errmsg);
771             $this->{serial} = 0;
772             return (undef, $errmsg);
773             }
774              
775             $this->{serial} = $obj;
776             return $obj;
777             }
778              
779              
780             # PRIVATE to _makeURI()
781             my %_char2field = (v => "volume",
782             i => "issue",
783             p => "spage",
784             t => "atitle",
785             I => "issn",
786             a => "aulast",
787             A => "auinit",
788             j => "isbn",
789             );
790              
791             # The format of recipes is described in ../../../doc/recipes
792             sub _makeURI {
793             my $this = shift();
794             my($recipe) = @_;
795             my $saved = $recipe;
796              
797             my $uri = "";
798             while ($recipe =~ s/(.*?)%([*_]*)(0?)([0-9]*)(([a-zA-Z%])|({[a-zA-Z_\\.\/]+}))//) {
799             my($head, $strip, $zero, $width, $item) = ($1, $2, $3, $4, $5);
800             $uri .= $head;
801             if ($item eq "%") {
802             $uri .= "%";
803             next;
804             }
805              
806             my $key;
807             if ($item =~ s/^{(.*)}$/$1/) {
808             $key = $item;
809             } else {
810             $key = $_char2field{$item};
811             }
812              
813             my $val;
814             if (!defined $key) {
815             $this->warn("recipe '$saved' used unknown item '$item'");
816             $val = "{UNKNOWN-$item}";
817             } else {
818             foreach my $onekey (split /\//, $key) {
819             $val = $this->_singleDatum($onekey);
820             last if defined $val && $val ne "";
821             }
822             return undef if !defined $val;
823             $val =~ s/-//g if $strip =~ /\*/;
824             $val =~ s/ //g if $strip =~ /_/;
825             }
826              
827             my $len = length($val);
828             if ($width ne "" && $len < $width) {
829             $val = (($zero eq "0" ? "0" : " ") x ($width-$len)) . $val;
830             }
831              
832             $uri .= $val;
833             }
834              
835             return $uri . $recipe;
836             }
837              
838              
839             sub _singleDatum {
840             my $this = shift();
841             my($key) = @_;
842              
843             if ($key eq "THIS") {
844             return $this->v10url("svc_dat");
845             } elsif ($key =~ /(.*?)([_\.])(.*)/) {
846             # Explicit descriptor specified
847             my($dname, $sep, $vname) = ($1, $2, $3);
848             my $descriptor = $this->descriptor($dname);
849             if ($sep eq "_") {
850             return defined $descriptor ?
851             $descriptor->superdata1($vname) : undef;
852             } else {
853             return defined $descriptor ?
854             $descriptor->metadata1($vname) : undef;
855             }
856             } else {
857             # No descriptor specified, use the defult: referent
858             return $this->rft($key);
859             }
860             }
861              
862              
863             =head3 resolve_to_xml()
864              
865             Returns the text of an ultra-simple XML document that contains all the
866             results. There is a DTD for this XML format in
867             C, but informally:
868              
869             =over 4
870              
871             =item *
872              
873             The document consists of a top-level C<> element containing
874             zero or more C<> elements.
875              
876             =item *
877              
878             Each result has mandatory C, C and C attributes
879             and optional C and C attributes
880              
881             =item *
882              
883             Each C<> element contains text which is typically but not
884             always a URI.
885              
886             =back 4
887              
888             =cut
889              
890             ### We should really use an XML-writer module rather than doing this
891             # by hand. In particular, it's misleading that the _xmlencode()
892             # routine is responsible for the UTF-8 encoding of values.
893             sub resolve_to_xml {
894             my $this = shift();
895              
896             my $xml = <<__EOT__;
897            
898            
899             __EOT__
900             foreach my $d ($this->co()->descriptors()) {
901             my $gotOne = 0;
902             foreach my $key ($d->metadata_keys()) {
903             $xml .= " name()) . "\">\n"
904             if !$gotOne++;
905             next if $key eq "title"; # we've copied this into "jtitle"
906             my $valref = $d->metadata($key);
907             foreach my $val (@$valref) {
908             $xml .= (" " .
909             _xmlencode($val) . "\n");
910             }
911             }
912             $xml .= " \n"
913             if $gotOne;
914             }
915              
916             foreach my $res ($this->resolve_to_results()) {
917             my $service = $res->service();
918             my $mimetype = $res->mimeType();
919             $xml .= "
920             $xml .= " type=\"" . _xmlencode($res->type()) . "\"";
921             $xml .= " priority=\"" . _xmlencode($res->priority()) . "\""
922             if defined $res->priority();
923             # In rational databases such as MySQL, NULL values are NULL,
924             # and are distinct from empty strings. Therefore, NULL fields
925             # can be omitted here. In Oracle, though, that distinction is
926             # steamrollered, and all empty fields become NULL. So in
927             # order to maintain the same output irrespective of which
928             # RDBMS we're using (so that the same regression-tests work
929             # for both), we need to emit an empty string even when the tag
930             # is actually NULL. *sigh*
931             my $tag = $res->tag();
932             $tag = "" if !defined $tag;
933             $xml .= " tag=\"" . _xmlencode($tag) . "\"";
934             $xml .= "\n\t" if defined $service || defined $mimetype;
935             $xml .= "service=\"" . _xmlencode($service) . "\""
936             if defined $service;
937             $xml .= " " if defined $service && defined $mimetype;
938             $xml .= "mimetype=\"" . _xmlencode($mimetype) . "\""
939             if defined $mimetype;
940             $xml .= "\n\t>" . _xmlencode($res->text()) . "\n";
941             }
942             $xml .= "\n";
943             return $xml;
944             }
945              
946              
947             # PRIVATE to resolve_to_xml()
948             sub _xmlencode {
949             my($x) = @_;
950              
951             $x = encode_utf8($x);
952             $x =~ s/&/&/g;
953             $x =~ s/
954             $x =~ s/>/>/g;
955             $x =~ s/\"/"/g;
956              
957             return $x;
958             }
959              
960              
961             =item resolve_to_html()
962              
963             Returns the text of an HTML document made by processing the XML
964             described above by a stylesheet. If an argument is given, then this
965             is taken as the basename of the stylesheet to use, to be found in the
966             the XSLT directory of the resolver (as specified by its C
967             option). If this is omitted, the stylesheet named by the
968             C option, from this directory, is used.
969              
970             =cut
971              
972             sub resolve_to_html {
973             my $this = shift();
974             my($ssname) = @_;
975              
976             $ssname = $this->option("xslt")
977             if !defined $ssname;
978              
979             my $parser = $this->resolver()->parser();
980             my $source = $parser->parse_string($this->resolve_to_xml());
981              
982             my $stylesheet = $this->resolver()->stylesheet($ssname);
983             my $result = $stylesheet->transform($source);
984             return $stylesheet->output_string($result);
985             }
986              
987              
988             =item resolve()
989              
990             Returns an array of two elements from which an entire HTTP response
991             can be built: the C and the actual content. The
992             response is XML, as returned from C, if the C
993             option is set and non-zero; or HTML otherwise, as returned from
994             C, otherwise.
995              
996             =back
997              
998             =cut
999              
1000             sub resolve {
1001             my $this = shift();
1002              
1003             my @res = $this->resolve_to_results();
1004             if (@res == 1 && $res[0]->single()) {
1005             # The OpenURL requested only a single result, so we return it
1006             # as its own object rather than embedded into a larger XML or
1007             # HTML document.
1008             my $mimeType = $res[0]->mimeType();
1009             $mimeType = "text/plain" if !defined $mimeType;
1010             return ($mimeType, $res[0]->text());
1011             }
1012              
1013             if ($this->option("xml")) {
1014             return ("text/xml", $this->resolve_to_xml());
1015             } else {
1016             return ("text/html; charset=UTF-8", $this->resolve_to_html());
1017             }
1018             }
1019              
1020              
1021             =head2 v10url()
1022              
1023             $url = $openurl->v10url("svc_dat", "rft_id");
1024              
1025             Returns a string containing a version 1.0 OpenURL representing the
1026             Context Object described by C<$openurl>. If arguments are provided,
1027             they are the names of keys to be omitted from the returned OpenURL.
1028              
1029             =cut
1030              
1031             sub v10url {
1032             my $this = shift();
1033             my(@skip) = @_;
1034              
1035             my $url = $this->base();
1036             my $gotOne = 0;
1037             foreach my $d ($this->co()->descriptors()) {
1038             my $name = $d->name();
1039             foreach my $key ($d->superdata_keys()) {
1040             my $valref = $d->superdata($key);
1041             foreach my $val (@$valref) {
1042             my $fullkey = "${name}_$key";
1043             next if grep { $_ eq $fullkey } @skip;
1044             $url .= $gotOne++ ? "&" : "?";
1045             $url .= "$fullkey=" . uri_escape_utf8($val);
1046             }
1047             }
1048             foreach my $key ($d->metadata_keys()) {
1049             my $valref = $d->metadata($key);
1050             foreach my $val (@$valref) {
1051             my $fullkey = "${name}.$key";
1052             next if grep { $_ eq $fullkey } @skip;
1053             $url .= $gotOne++ ? "&" : "?";
1054             $url .= "$fullkey=" . uri_escape_utf8($val);
1055             }
1056             }
1057             }
1058              
1059             return $url;
1060             }
1061              
1062              
1063             1;