File Coverage

blib/lib/REST/Client/CrossRef.pm
Criterion Covered Total %
statement 286 406 70.4
branch 99 182 54.4
condition 15 27 55.5
subroutine 36 47 76.6
pod 15 17 88.2
total 451 679 66.4


line stmt bran cond sub pod time code
1             package REST::Client::CrossRef;
2 1     1   172140 use strict;
  1         3  
  1         31  
3 1     1   6 use warnings;
  1         2  
  1         31  
4 1     1   548 use Moo;
  1         10925  
  1         5  
5            
6 1     1   2105 use JSON;
  1         9609  
  1         5  
7 1     1   562 use URI::Escape;
  1         1299  
  1         62  
8 1     1   449 use REST::Client;
  1         46154  
  1         41  
9            
10             #use Data::Dumper;
11 1     1   8 use Carp;
  1         2  
  1         56  
12 1     1   481 use Log::Any;
  1         8201  
  1         5  
13 1     1   507 use HTTP::Cache::Transparent;
  1         14315  
  1         8  
14            
15             #use JSON::MultiValueOrdered;
16             #use YAML;
17 1     1   514 use JSON::Path;
  1         50951  
  1         13  
18            
19 1     1   622 use namespace::clean;
  1         9358  
  1         6  
20            
21             =head1 NAME
22            
23             REST::Client::CrossRef - Read data from CrossRef using its REST API
24            
25             =cut
26            
27             our $VERSION = '0.008';
28            
29             =head1 VERSION
30            
31             Version 0.008
32            
33             =cut
34            
35             =head1 DESCRIPTION
36            
37             This module use L to read the data from the CrossRef repository.
38            
39             =cut
40            
41             =head1 SYNOPSIS
42            
43             use Log::Any::Adapter( 'File', './log.txt', 'log_level'=> 'info');
44             use REST::Client::CrossRef;
45            
46             #the mail address is added in the request's header
47             #return the data without transformation
48            
49             my $cr = REST::Client::CrossRef->new(
50             mailto => 'you@somewhre.com',
51             spit_raw_data => 1,
52             );
53            
54             #cache the data with HTTP::Cache::Transparent
55             $cr->init_cache(
56             { BasePath => ".\cache",
57             NoUpdate => 60 * 60,
58             verbose => 0
59             });
60            
61             my $data = $cr->journal_from_doi('10.1088/0004-637X/722/2/971');
62            
63             print Dumper($data), "\n"; #$data is a hash ref of the json data converted to perl
64            
65             #unfold the data to something like
66             # field1/subfield1/subfield2 : value
67             #add an undef value after each item fields
68             #output only the fields given with keys_to_keep, with the same ordering
69            
70             my $cr = REST::Client::CrossRef->new(
71             mailto => 'you@somewhere.com',
72             add_end_flag => 1,
73             keys_to_keep => [
74             ['author'], ['title'], ['container-title'],
75             ['volume'],['issue'], ['page'],['issued/date-parts'], ['published-print/date-parts']
76             ],);
77            
78             my $data = $cr->article_from_doi('10.1088/0004-637X/722/2/971');
79            
80             for my $row (@$data) {
81             if (! $row) {
82             print "\n";
83             next;
84             }
85             while ( my ($f, $v) = each %$row) {
86             print "$f : $v \n";
87             }
88             }
89            
90            
91             #display the item's fields in alphabetic order
92             #add 'end of data' field after each item
93            
94             my $cr = REST::Client::CrossRef->new(
95             mailto => 'you@somewhre.com',
96             add_end_flag => 1,
97             sort_output => 1,
98             );
99            
100             $cr->init_cache(
101             { BasePath => "C:\\Windows\\Temp\\perl",
102             NoUpdate => 60 * 60,
103             verbose => 0
104             });
105            
106             my @fields = (qw/author title/);
107             my @values = (qw/allan electron/);
108            
109             #return 100 items by page
110            
111             $cr->rows(100);
112             my $data = $cr->query_articles( \@fields, \@values );
113             while () {
114             last unless $data;
115            
116             for my $row (@$data) {
117             print "\n" unless ($row);
118             for my $field (keys %$row) {
119             print $field, ": ", $row->{$field}. "\n";
120             }
121             }
122             $data = $cr->get_next();
123             }
124            
125             Example output:
126            
127             author : Wilke, Ingrid;
128             MacLeod, Allan M.;
129             Gillespie, William A.;
130             Berden, Giel;
131             Knippels, Guido M. H.;
132             van der Meer, Alexander F. G.;
133             container-title : Optics and Photonics News
134             issue : 12
135             issued/date-parts : 2002, 12, 1,
136             page : 16
137             published-online/date-parts : 2002, 12, 1,
138             published-print/date-parts : 2002, 12, 1,
139             title : Detectors: Time-Domain Terahertz Science Improves Relativistic Electron-Beam Diagnostics
140             volume : 13
141            
142             my $cr = REST::Client::CrossRef->new(
143             mailto => 'dokpe@unifr.ch',
144             spit_raw_data => 0,
145             add_end_flag => 1,
146             json_path => [
147             ['$.author[*]'],
148             ['$.title'],
149             ['$.container-title'],
150             ['$.volume'], ['$.issue'], ['$.page'],
151             ['$.issued..date-parts'],
152             ['$.published-print..date-parts']
153             ],
154             json_path_callback => { '$.items[*].author[*]' => \&unfold_authors },
155             );
156            
157             sub unfold_authors {
158             my ($data_ar) = @_;
159             my @res;
160             for my $aut (@$data_ar) {
161             my $line;
162             if ( $aut->{affiliation} ) {
163             my @aff;
164             for my $hr ( @{$aut->{affiliation}} ) {
165             my @aff = values %$hr;
166             $aff[0] =~ s/\r/ /g;
167             $line .= " " . $aff[0];
168             }
169             }
170             my $fn = (defined $aut->{given}) ?( ", " . $aut->{given} . "; " ): "; ";
171             push @res, $aut->{family} . $fn . ($line // "");
172             }
173             return \@res;
174             }
175            
176             my $data = $cr->article_from_doi($doi);
177             next unless $data;
178             for my $row (@$data) {
179             if ( !$row ) {
180             print "\n";
181             next;
182             }
183             while ( my ( $f, $v ) = each %$row ) {
184             print "$f : $v \n";
185             }
186             }
187            
188             Example of output:
189             $.author[*] : Pelloni, Michelle; University of Basel, Department of Chemistry, Mattenstrasse 24a, BPR 1096, CH 4002 Basel, Switzerland
190             Cote, Paul; School of Chemistry and Biochemistry, University of Geneva, Quai Ernest Ansermet 30, CH-1211 Geneva, Switzerland
191             ....
192             Warding, Tom.; University of Basel, Department of Chemistry, Mattenstrasse 24a, BPR 1096, CH 4002 Basel, Switzerland
193             $.title : Chimeric Artifact for Artificial Metalloenzymes
194             $.container-title : ACS Catalysis
195             $.volume : 8
196             $.issue : 2
197             $.page : 14-18
198             $.issued..date-parts : 2018, 1, 24
199             $.published-print..date-parts : 2018, 2, 2
200            
201             my $cr = REST::Client::CrossRef->new( mailto => 'you@somewher.com'
202             ,keys_to_keep => [["breakdowns/id", "id"], ["location"], [ "primary-name", "breakdowns/primary-name", "name" ]],
203             );
204            
205             $cr->init_cache(
206             { BasePath => "C:\\Windows\\Temp\\perl",
207             NoUpdate => 60 * 60,
208             verbose => 0
209             });
210            
211             $cr->rows(100);
212            
213             my $rs_ar = $cr->get_members;
214            
215             while () {
216             last unless $rs_ar;
217             for my $row_hr (@$rs_ar) {
218             for my $k (keys %$row_hr) {
219             print $k . " : " . $row_hr->{$k} . "\n";
220             }
221             }
222             $rs_ar = $cr->get_next();
223             }
224            
225             Example of items in the output above
226            
227             id : 5007
228             location : W. Struve 1 Tartu 50091 Estonia
229             primary-name : University of Tartu Press
230            
231             id : 310
232             location : 23 Millig Street Helensburgh Helensburgh Argyll G84 9LD United Kingdom
233             primary-name : Westburn Publishers
234            
235             id : 183
236             location : 9650 Rockville Pike Attn: Lynn Willis Bethesda MD 20814 United States
237             primary-name : Society for Leukocyte Biology
238            
239             =cut
240            
241             has baseurl => ( is => 'ro', default => sub {'https://api.crossref.org'} );
242             has modified_since => ( is => 'ro' );
243            
244             #has version => (is => 'ro', default => sub {'v1'} );
245            
246             has rows => (
247             is => 'rw',
248             default => sub {0},
249             isa => sub { croak "rows must be under 1000" unless $_[0] < 1000 }
250             );
251             has code => ( is => 'rw' );
252             has sleep => ( is => 'rw', default => sub {0} );
253             has log => ( is => 'lazy' );
254             has client => ( is => 'lazy' );
255             has decoder => ( is => 'lazy' );
256            
257             =head2 C<$cr = REST::Client::CrossRef-Enew( ... mailto =E your@email.here, ...)>
258            
259             The email address is placed in the header of the page.
260             See L
261            
262             =cut
263            
264             has mailto => ( is => 'lazy', default => sub {0} );
265            
266             =head2 C<$cr = REST::Client::CrossRef-Enew( ... sort_output =E1, ...)>
267            
268             Rows can be sorted using the key name with sort_ouput => 1.
269             Default to 0.
270             In effect only if C is false.
271            
272             =cut
273            
274             has sort_output => ( is => 'lazy', default => sub {0} );
275             has test_data => ( is => 'lazy', default => sub {0} );
276            
277             =head2 C<$cr = REST::Client::CrossRef-Enew( ... spit_raw_data =E1, ...)>
278            
279             Display the data as a hashref if 0 or as an array ref of hasref,
280             where each hashref is a row of key => value that can be sorted with sort_ouput => 1.
281             C default to 0.
282            
283             =cut
284            
285             has spit_raw_data => ( is => 'lazy', default => sub {0} );
286             has cursor => ( is => 'rw' );
287             has page_start_at => ( is => 'rw', default => sub {0} );
288            
289             =head2 C<$cr = REST::Client::CrossRef-Enew( ... add_end_flag =E1, ...)>
290            
291             Add undef after an item's fields.
292             Default to 1.
293            
294             =cut
295            
296             has add_end_flag => ( is => 'lazy', default => sub {1} );
297            
298             =head2 C<$cr = REST::Client::CrossRef-Enew( ... keys_to_keep =E [[key1, key1a, ...], [key2], ... ], ...)>
299            
300             An array ref of array ref, the inner array ref give a key name and the possible alternative keys for the same value,
301             for example [ "primary-name", "breakdowns/primary-name", "name" ] in the member road (url ending with /members).
302             The keys enumeration starts below C, or C - C if the result is a list.
303             This filters the values that are returned and preserves the ordering of the array ref given in argument.
304             The ouput is an array ref of hash ref, each hash having the key and the values.
305             Values are flattened as string. In effect only if spit_raw_data is false.
306            
307             =cut
308            
309             has keys_to_keep => ( is => 'lazy' );
310            
311             =head2 C<$cr = REST::Client::CrossRef-Enew( ... json_path =E [[$path1, path1a, ...], [path2], ... ], ...)>
312            
313             An array ref of array ref, the inner array refs give a L
314             and the possible alternative path for the same value. See also L.
315             The json path starts below C, or C - C if the result is a list.
316             The output, ordering, filtering and flattening is as above. In effect only if spit_raw_data is false.
317            
318             =cut
319            
320             has json_path => ( is => 'lazy' );
321            
322             =head2 C<$cr = REST::Client::CrossRef-Enew( ... json_path_callback =E {$path =E \&some_function }>
323            
324             An hash ref that associates a JSON path and a function that will be run on the data return by C<$jpath-Evalues($json_data)>.
325             The function must accept an array ref as first argument and must return an array ref.
326            
327             =cut
328            
329             has json_path_callback => ( is => 'lazy' );
330            
331             =head2 C<$cr = REST::Client::CrossRef-Enew( ... json_path_safe =E "0", ... )>
332            
333             To turn off the message C set this to 0.
334             Default to 1.
335            
336             =cut
337            
338             has json_path_safe => (is => 'lazy', default=> sub{1});
339            
340             =head2 C<$cr = REST::Client::CrossRef-Enew( ... version =E "v1", ... )>
341            
342             To use a defined version of the api.
343             See L
344            
345             =cut
346            
347             has version => ( is => 'ro' );
348            
349             sub _build_client {
350 2     2   19 my ($self) = @_;
351 2         19 my $client = REST::Client->new();
352            
353             #HTTP::Cache::Transparent::init( { BasePath => './cache', NoUpdate => 15*60, verbose=>1 } );
354             #$self->cache()
355            
356 2 50       5762 if ( $self->version ) {
357 0         0 $self->log->notice( "< Crossref-API-Version: " . $self->version );
358 0         0 $client->addHeader( 'api-version', $self->version );
359             }
360 2 50       41 if ( defined $self->mailto ) {
361            
362             #my $authorization = 'Bearer ' . $self->key;
363 2         35 $self->log->notice( "< Mailto: " . $self->mailto );
364 2         102 $client->addHeader( 'mailto', $self->mailto );
365            
366             }
367            
368 2         48 $client;
369             }
370            
371             sub _build_decoder {
372 4     4   40 my $self = shift;
373 4         1091 return JSON->new;
374            
375             #return JSON::MultiValueOrdered->new;
376            
377             }
378            
379             sub _build_log {
380 4     4   39 my ($self) = @_;
381 4         34 Log::Any->get_logger( category => ref($self) );
382             }
383            
384             =head2 C<$cr-Einit_cache( @args )> C<$cr-Einit_cache( $hash_ref )>
385            
386             See L.
387             The array of args is passed to the object constructor.
388             The log file shows if the data has been fetch from the cache and if the server has been queryied to detect any change.
389            
390             =cut
391            
392             sub init_cache {
393 0     0 1 0 my ( $self, @args ) = @_;
394 0         0 my $href;
395 0 0       0 if ( ref $args[0] eq "HASH" ) {
396 0         0 $href = $args[0];
397             }
398             else {
399 0         0 my %h;
400 0         0 %h = @args;
401 0         0 $href = \%h;
402             }
403 0         0 HTTP::Cache::Transparent::init($href);
404             }
405            
406             sub _build_filter {
407 4     4   9 my ( $self, $ar ) = @_;
408            
409             #die Dumper $self->{keys_to_keep};
410             # die "ar:" . Dumper $ar;
411 4         8 my %filter;
412 4         11 for my $filter_name (qw(keys_to_keep json_path)) {
413            
414             # my %keys_to_keep;
415 8 100       65 next if ( !exists $ar->{$filter_name} );
416            
417             #print "_build_filter: $filter_name\n";
418 3         11 my $pos;
419             my %pos_seen;
420 3         0 my %key_seen;
421            
422 3         6 for my $ar ( @{ $self->{$filter_name} } ) {
  3         9  
423 6         11 $pos++;
424 6         10 for my $k (@$ar) {
425 6         18 $filter{$k} = $pos - 1;
426 6         14 $pos_seen{ $pos - 1 } = 0;
427 6         15 $key_seen{ $pos - 1 } = $k;
428             }
429            
430             }
431 3         10 $self->{pos_seen} = \%pos_seen;
432 3         8 $self->{key_seen} = \%key_seen;
433 3         68 $self->{$filter_name} = \%filter;
434             }
435            
436             }
437            
438             sub BUILD {
439            
440 4     4 0 45 my ( $self, $ar ) = @_;
441             croak "Can't use both keys_to_keep and json_path"
442 4 50 66     20 if ( $ar->{json_path} && $ar->{keys_to_keep} );
443 4         13 $self->_build_filter($ar);
444             }
445            
446             sub _crossref_get_request {
447 9     9   52 my ( $self, $path, $query_ar, %param ) = @_;
448 9 100       155 return 1 if ( $self->test_data() );
449 7 50       129 my $url = sprintf "%s%s%s", $self->baseurl,
450             $self->version ? "/" . $self->version : "", $path;
451            
452 7         23 my @params = ();
453            
454 7 50       24 if ($query_ar) {
455            
456 0         0 for my $p (@$query_ar) {
457            
458             #print "$p\n";
459 0         0 push @params, $p;
460            
461             }
462             }
463 7         28 for my $name ( keys %param ) {
464 14         27 my $value = $param{$name};
465            
466             #location:United Kingdom space is uri_escape twice
467             #push @params, uri_escape($name) . "=" . uri_escape($value) if ($value);
468 14 100       59 push @params, $name . "=" . $value if ($value);
469            
470             }
471 7 100       136 push @params, "rows=" . $self->rows if ( $self->rows );
472 7 100       195 push @params, "cursor=" . $self->cursor if ( $self->cursor );
473            
474             #if the first url as &offset=1 we missed the first item
475             #1 in page_start_at means "paginate with offset"
476             #offset : page_start_at -1
477 7 50 33     32 push @params, "offset=" . ( $self->page_start_at - 1 )
478             if ( $self->page_start_at && $self->page_start_at > $self->rows );
479 7 100       51 $url .= '?' . join( "&", @params ) if @params > 0;
480            
481             #die Dumper @params;
482             # The server asked us to sleep..
483 7 50       37 if ( $self->sleep > 0 ) {
484 0         0 $self->log->notice( "sleeping: " . $self->sleep . " seconds" );
485 0         0 sleep $self->sleep;
486 0         0 $self->sleep(0);
487             }
488 7         119 $self->log->notice(" ");
489 7         201 $self->log->notice("requesting: $url");
490 7         187 my $response = $self->client->GET($url);
491 7         13558472 my $val = $response->responseHeader('Backoff');
492 7 50       512 my $backoff = defined $val ? $val : 0;
493 7         29 $val = $response->responseHeader('Retry-After');
494 7 50       335 my $retryAfter = defined $val ? $val : 0;
495 7         37 my $code = $response->responseCode();
496            
497 7         453 $self->log->notice("> Code: $code");
498 7         291 $self->log->notice("> Backoff: $backoff");
499 7         189 $self->log->notice("> Retry-After: $retryAfter");
500 7         88 for my $k (qw/X-Cached X-Content-Unchanged X-No-Server-Contact/) {
501 21 50       916 $self->log->notice( "> $k: " . $response->responseHeader($k) )
502             if $response->responseHeader($k);
503             }
504            
505 7 50 33     467 if ( $backoff > 0 ) {
    50          
506 0         0 $self->sleep($backoff);
507             }
508             elsif ( $code eq '429' || $code eq '503' ) {
509 0 0       0 $self->sleep( defined $retryAfter ? $retryAfter : 60 );
510 0         0 return;
511             }
512            
513 7         166 $self->log->notice( "> Content: " . substr($response->responseContent, 0, 50) );
514            
515 7         296 $self->code($code);
516            
517 7 50       32 return unless $code eq '200';
518            
519 7         56 $response;
520             }
521            
522             #pagination using cursor
523             #page_start_set to 0 to insure that get_next called this function again
524             sub _get_metadata {
525 8     8   48 my ( $self, $path, $query_ar, $filter, $select ) = @_;
526 8 100       151 $self->log->notice( "test_data: ",
527             ( $self->test_data() ? " 1 " : " 0 " ) );
528 8         2428 $self->page_start_at(0);
529 8         40 my $response =
530             $self->_crossref_get_request( $path,
531             $query_ar, ( filter => $filter, select => $select ) );
532            
533             # print Dumper $response;
534 8 50       44 return unless $response;
535            
536             #my $hr = decode_json $response->responseContent;
537            
538 8 100       195 my $hr =
539             $self->test_data()
540             ? $self->_decode_json( $self->test_data() )
541             : $self->_decode_json( $response->responseContent );
542 8         34 my $res_count = $hr->{message}->{'total-results'};
543            
544             #print $res_count;
545 8 100 66     60 if ( defined $res_count && $res_count > 0 ) {
546            
547             # my $keys;
548             # my $data_ar = $hr->{message}->{items};
549 7         22 my $returned_items = @{ $hr->{message}->{items} };
  7         20  
550 7         43 $self->_set_cursor( $hr->{message}, $returned_items );
551            
552             #push @$keys, "items";
553             #die $self->spit_raw_data;
554             #$self->_display_data($hr);
555             }
556 8         221 return $self->_display_data($hr);
557             }
558            
559             #pagination using page_start_at + offset
560             #curosr set to undef to insure that get_next called this function again
561             sub _get_page_metadata {
562 1     1   3 my ( $self, $path, $param_ar ) = @_;
563            
564 1         3 my $response;
565             my $out;
566 1         16 $self->cursor(undef);
567 1 50       4 if ($param_ar) {
568 0         0 my $filter = join( ",", @$param_ar );
569            
570 0         0 $response =
571             $self->_crossref_get_request( $path, undef,
572             ( filter => $filter ) );
573             }
574             else {
575 1         5 $response = $self->_crossref_get_request($path);
576             }
577            
578 1 50       14 if ($response) {
579            
580             #my $hr = decode_json $response->responseContent;
581 1 50       16 my $hr =
582             $self->test_data()
583             ? $self->_decode_json( $self->test_data() )
584             : $self->_decode_json( $response->responseContent );
585 1         4 my $res_count = $hr->{message}->{'total-results'};
586            
587 1 50       4 if ( defined $res_count ) {
588            
589             # print "from metadata: ", $res_count, "\n";
590 0 0       0 if ( $res_count > 0 ) {
591            
592             #die Dumper $hr->{message}->{items};
593 0         0 my $returned_items_count = @{ $hr->{message}->{items} };
  0         0  
594            
595 0         0 $self->{last_page_items_count} = $returned_items_count;
596 0         0 $out = $self->_display_data($hr);
597            
598             }
599             }
600             else { #singleton
601 1         4 $out = $self->_display_data($hr);
602            
603             }
604             }
605            
606 1         4 return $out;
607            
608             }
609            
610             sub _display_data {
611 9     9   32 my ( $self, $hr ) = @_;
612            
613 9 100       203 return $hr if ( $self->spit_raw_data );
614 8         152 my $formatter = REST::Client::CrossRef::Unfolder->new();
615            
616 8         34 my $data_ar;
617 8 100       32 if ( $hr->{message}->{items} ) {
618 6         19 $data_ar = $hr->{message}->{items};
619             }
620             else {
621 2         5 $data_ar = [ $hr->{message} ];
622             }
623            
624 8         17 my @data;
625 8 100       38 if ( defined $self->{json_path} ) {
    50          
626            
627 6         10 my %result;
628 6         14 my %keys = %{ $self->{json_path} };
  6         38  
629 6         14 my %selectors;
630 6         148 $JSON::Path::Safe=$self->json_path_safe;
631 6         92 for my $path ( keys %keys ) {
632            
633             #print $path, "\n";
634 18         194 $selectors{$path} = JSON::Path->new($path);
635             }
636            
637 6         53 for my $data_hr (@$data_ar) {
638            
639 30         88 for my $path ( keys %selectors ) {
640            
641             #my @val = $jpath->values( $hr->{message} );
642 90         326 my @val = $selectors{$path}->values($data_hr);
643            
644 90 50 33     222285 if ( $self->{json_path_callback}
    50          
645             && $self->{json_path_callback}->{$path} )
646             {
647 0         0 my @data;
648 0         0 my $cb = $self->{json_path_callback}->{$path};
649 0 0       0 eval { @data = @{ $cb->( \@val ) } if(@val); };
  0         0  
  0         0  
650 0 0       0 carp "Json callback failed : $@\n" if ($@);
651 0         0 $result{$path} = join( "\n", @data );
652            
653             }
654             elsif (@val) {
655            
656 90         181 my %res_part;
657             %res_part =
658 90         138 %{ $formatter->_unfold_array( \@val, [$path] ) };
  90         318  
659 90         467 @result{ keys %res_part } = values %res_part;
660             }
661            
662             }
663            
664             push @data,
665 30         57 @{ $self->_sort_output( $self->{json_path}, \%result ) };
  30         107  
666             }
667            
668             }
669             elsif ( defined $self->{keys_to_keep} ) {
670            
671 2         5 my $new_ar;
672            
673             #$data_ar :array ref of rows items
674 2         9 $formatter->set_keys_to_keep( $self->{keys_to_keep} );
675            
676 2         5 for my $data_hr (@$data_ar) {
677            
678             #https://www.perlmonks.org/?node_id=1224994
679             #my $result_hr = {};
680             #$formatter->_unfold_hash($data_hr, undef, $result_hr);
681 2         6 my $result_hr = $formatter->_unfold_hash($data_hr);
682            
683             # $self->log->debug("display_data\n", Dumper $result_hr);
684             push @data,
685 2         4 @{ $self->_sort_output( $self->{keys_to_keep}, $result_hr ) };
  2         8  
686            
687             }
688            
689             }
690             else { #neither json_path nor keys_to_keep defined, spit_raw_data set to 0
691            
692 0         0 for my $data_hr (@$data_ar) {
693 0         0 my $val_hr = $formatter->_unfold_hash($data_hr);
694 0         0 my @keys;
695 0 0       0 if ( $self->{sort_output} ) {
696 0         0 @keys = sort { lc($a) cmp lc($b) } keys %$val_hr;
  0         0  
697             }
698             else {
699 0         0 @keys = keys %$val_hr;
700             }
701 0         0 for my $k (@keys) {
702 0         0 push @data, { $k, $val_hr->{$k} };
703             }
704            
705 0 0       0 push @data, undef if $self->add_end_flag;
706             }
707            
708             }
709 8         482 return \@data;
710             }
711            
712             sub _sort_output {
713 32     32   87 my ( $self, $filter_hr, $result_hr ) = @_;
714            
715 32         62 my @data;
716            
717 32 50       79 if ($filter_hr) {
718            
719 32         66 my %keys_to_keep = %{$filter_hr};
  32         121  
720 32         63 my %pos_seen = %{ $self->{pos_seen} };
  32         121  
721 32         73 my %key_seen = %{ $self->{key_seen} };
  32         120  
722 32         129 $pos_seen{$_} = 0 foreach ( keys %pos_seen );
723            
724 32         63 my @item_data;
725 32         109 for my $key ( keys %$result_hr ) {
726            
727 244         350 my $pos = $keys_to_keep{$key};
728 244 100       518 next unless defined $pos;
729            
730             # print "pos undef for $key\n" unless defined $pos;
731             #$key_unseen{$pos}= $key;
732 93         186 $pos_seen{$pos} = 1;
733             my $value =
734             ( defined $result_hr->{$key} )
735 93 50       203 ? $result_hr->{$key}
736             : "";
737 93         1931 $self->log->debug( $key, " - ", $value );
738 93         1060 $item_data[$pos] = { $key => $value };
739            
740             }
741            
742 32         113 my @unseen = grep { !$pos_seen{$_} } keys %pos_seen;
  93         271  
743            
744 32         78 for my $pos (@unseen) {
745 0         0 $item_data[$pos] = { $key_seen{$pos}, "" };
746            
747             }
748 32         77 push @data, @item_data;
749 32 100       570 push @data, undef if $self->add_end_flag;
750            
751             }
752             else {
753 0         0 my @keys;
754 0 0       0 if ( $self->{sort_output} ) {
755 0         0 @keys = sort { lc($a) cmp lc($b) } keys %$result_hr;
  0         0  
756             }
757             else {
758 0         0 @keys = keys %$result_hr;
759             }
760 0         0 for my $k (@keys) {
761            
762 0         0 push @data, { $k, $result_hr->{$k} };
763             }
764            
765 0 0       0 push @data, undef if $self->add_end_flag;
766             }
767            
768 32         497 return \@data;
769            
770             }
771            
772             =head2 C<$cr-Erows( $row_value )>
773            
774             Set the rows parameter that determines how many items are returned in one page
775            
776             =cut
777            
778             =head2 C<$cr-Eworks_from_doi( $doi )>
779            
780             Retrive the metadata from the work road (url ending with works) using the article's doi.
781             Return undef if the doi is not found.
782             You may pass a select string with the format "field1,field2,..." to return only these fields.
783             Fields that may be use for selection are (October 2018):
784             abstract, URL, member, posted, score, created, degree, update-policy, short-title, license, ISSN,
785             container-title, issued, update-to, issue, prefix, approved, indexed, article-number, clinical-trial-number,
786             accepted, author, group-title, DOI, is-referenced-by-count, updated-by, event, chair, standards-body, original-title,
787             funder, translator, archive, published-print, alternative-id, subject, subtitle, published-online, publisher-location,
788             content-domain, reference, title, link, type, publisher, volume, references-count, ISBN, issn-type, assertion,
789             deposited, page, content-created, short-container-title, relation, editor.
790             Use keys_to_keep or json_path to define an ordering in the ouptut. Use select to filter the fields to be returned from the server.
791            
792             =cut
793            
794             sub works_from_doi {
795 0     0 1 0 my ( $self, $doi, $select ) = @_;
796 0 0       0 croak "works_from_doi: need doi" unless defined $doi;
797 0         0 $self->_get_metadata( "/works", undef, "doi:$doi", $select );
798             }
799            
800             =head2 C<$cr-Ejournal_from_doi( $doi )>
801            
802             A shortcut for C
803            
804             =cut
805            
806             sub journal_from_doi {
807 0     0 1 0 my ( $self, $doi ) = @_;
808 0 0       0 croak "journal_from_doi: need doi" unless defined $doi;
809 0         0 $self->_get_metadata( "/works", undef, "doi:$doi",
810             "container-title,page,issued,volume,issue" );
811            
812             }
813            
814             =head2 C<$cr-Earticle_from_doi( $doi )>
815            
816             A shortcut for C
817            
818             =cut
819            
820             sub article_from_doi {
821 1     1 1 705 my ( $self, $doi ) = @_;
822 1 50       6 croak "article_from_doi: need doi" unless defined $doi;
823 1         7 $self->_get_metadata( "/works", undef, "doi:$doi",
824             "title,container-title,page,issued,volume,issue,author,published-print,published-online"
825             );
826             }
827            
828             =head2 C<$cr-Earticle_from_funder( $funder_id, {name=E'smith'}, $select )>
829            
830             Retrive the metadata from the works road for a given funder, searched with an author's name or filtered by any valid filter name.
831             For example C<{'has-orcid'=E 'true', 'has-affiliation'=E'true'}>.
832             C<$select> default to "title,container-title,page,issued,volume,issue,published-print,DOI". Use * to retrieve all fields.
833            
834             =cut
835            
836             sub articles_from_funder {
837 1     1 0 17 my ( $self, $id, $href, $select ) = @_;
838            
839 1 50       4 croak "articles_from_funder: need funder id" unless defined $id;
840 1 50       4 $select = (
841             $select
842             ? $select
843             : "title,container-title,page,issued,volume,issue,published-print,DOI"
844             );
845 1 50       6 $self->{select} = $select eq "*" ? undef : $select;
846 1         5 $self->{path} = "/funders/$id/works";
847 1         4 $self->cursor("*");
848 1         3 my @filters;
849             my $query;
850 1         4 for my $k ( keys %$href ) {
851 2 50       51 if ( $k eq "name" ) {
    50          
852 0         0 $query = [ "query.author=" . uri_escape( $href->{$k} ) ];
853 0         0 $self->{param} = $query;
854             #return $self->_get_metadata( "/funders/$id/works", $query, undef, $self->{select} );
855             }
856             elsif ( $k eq "orcid" ) {
857             my $url =
858             $href->{$k} =~ /^https*:\/\/orcid.org\//
859             ? $href->{$k}
860 0 0       0 : "http://orcid.org/" . $href->{$k};
861 0         0 push @filters, "orcid:" . uri_escape($url);
862             #$self->{filter} = "orcid:" . uri_escape($url);
863             #return $self->_get_metadata( "/funders/$id/works", undef,$self->{filter}, $self->{select} );
864             }
865             else { #croak "articles_from_funder : unknown key : $k";
866             #$self->{filter} = $k . ":" . uri_escape( $href->{$k});
867             #return $self->_get_metadata( "/funders/$id/works", undef,$self->{filter}, $self->{select} );
868 2         9 push @filters, $k . ":" . uri_escape( $href->{$k});
869             }
870             }
871 1         15 $self->{filter} = join(",", @filters);
872            
873             return $self->_get_metadata( "/funders/$id/works", $query, $self->{filter},
874 1         6 $self->{select} );
875            
876             }
877            
878             =head2 C<$cr-Eget_types()>
879            
880             Retrieve all the metadata from the types road.
881            
882             =cut
883            
884             sub get_types {
885 1     1 1 6 my $self = shift;
886 1         4 $self->_get_metadata("/types");
887             }
888            
889             =head2 C<$cr-Eget_members()>
890            
891             Retrieve all the metadata (> 10'000 items) from the members road.
892            
893             =cut
894            
895             sub get_members {
896 0     0 1 0 my $self = shift;
897            
898 0         0 $self->page_start_at(1);
899 0         0 $self->{path} = "/members";
900 0         0 $self->_get_page_metadata("/members");
901            
902             }
903            
904             =head2 C<$cr-Emember_from_id( $member_id )>
905            
906             Retrieve a members from it's ID
907            
908             =cut
909            
910             sub member_from_id {
911 1     1 1 8 my ( $self, $id ) = @_;
912 1 50       4 croak "member_from_id: need id" unless ($id);
913 1         24 my $rows = $self->rows();
914 1         22 $self->rows(0);
915 1         12 my $rs = $self->_get_page_metadata("/members/$id");
916 1         22 $self->rows($rows);
917 1         10 return $rs;
918            
919             }
920            
921             =head2 C<$cr-Eget_journals()>
922            
923             Retrieve all the metadata (> 60'000 items) from the journals road.
924            
925             =cut
926            
927             sub get_journals {
928 0     0 1 0 my $self = shift;
929 0         0 $self->{path} = "/journals";
930 0         0 $self->page_start_at(1);
931 0         0 $self->_get_page_metadata("/journals");
932            
933             }
934            
935             =head2 C<$cr-Eget_licences()>
936            
937             Retrieve all the metadata (> 700 items) from the licenses road.
938            
939             =cut
940            
941             sub get_licences {
942 0     0 1 0 my $self = shift;
943 0         0 $self->{path} = "/licences";
944 0         0 $self->page_start_at(1);
945 0         0 $self->_get_page_metadata("/licences");
946            
947             }
948            
949             =head2 C<$cr-Equery_works( $fields_array_ref, $values_array_ref, $select_string )>
950            
951             See L for the fields that can be searched.
952             You may omit the "query." part in the field name.
953             The corresponding values are passed in a second array, in the same order.
954             Beware that searching with first and family name is treated as an OR not and AND:
955             C will retrieve all the works where and author has Tom in the name field or all works where an author has Smith in the name field.
956             See C above for the fields that can be selected.
957             Use keys_to_keep or json_path to define an ordering in the ouptut. Use select to filter the fields to be returned from the server.
958             =cut
959            
960             sub query_works {
961 0     0 1 0 my ( $self, $field_ar, $value_ar, $select ) = @_;
962 0         0 my $i;
963             my @params;
964 0         0 for my $field (@$field_ar) {
965 0 0       0 croak "unknown field $field"
966             unless ( $field
967             =~ /(?:container-)*title$|author$|editor$|chair$|translator$|contributor$|bibliographic$|affiliation$/
968             );
969 0 0       0 $field = "query." . $field unless ( $field =~ /^query\./ );
970 0         0 push @params, $field . "=" . uri_escape( $value_ar->[ $i++ ] );
971             }
972 0         0 $self->cursor("*");
973 0         0 $self->{path} = "/works";
974 0         0 $self->{param} = \@params;
975 0         0 $self->{select} = $select;
976 0         0 $self->_get_metadata( "/works", \@params, undef, $select );
977            
978             }
979            
980             =head2 C<$cr-Equery_articles( $fields_array_ref, $values_array_ref )>
981            
982             A shortcut for C<$cr-Equery_works($fields_array_ref, $values_array_ref, "title,container-title,page,issued,volume,issue,author,published-print,published-online")>
983            
984             =cut
985            
986             sub query_articles {
987 0     0 1 0 my ( $self, $field_ar, $value_ar ) = @_;
988 0         0 $self->query_works( $field_ar, $value_ar,
989             "title,container-title,page,issued,volume,issue,author,published-print,published-online"
990             );
991             }
992            
993             =head2 C<$cr-Equery_journals( $fields_array_ref, $values_array_ref )>
994            
995             A shortcut for C<$cr-Equery_works($fields_array_ref, $values_array_ref, "container-title,page,issued,volume,issue">
996            
997             =cut
998            
999             sub query_journals {
1000 0     0 1 0 my ( $self, $field_ar, $value_ar ) = @_;
1001 0         0 $self->query_works( $field_ar, $value_ar,
1002             "container-title,page,issued,volume,issue" );
1003            
1004             }
1005            
1006             =head2 C<$cr-Eget_next()>
1007            
1008             Return the next set of data in the /works, /members, /journals, /funders, /licences roads,
1009             Return undef after the last set.
1010            
1011             =cut
1012            
1013             sub get_next {
1014 5     5 1 997 my $self = shift;
1015 5 50       140 $self->log->debug( "get_next cursor: ",
1016             ( defined $self->cursor ? " defined " : " undef" ) );
1017 5         156 $self->log->debug( "get_next page_start_at: ", $self->page_start_at );
1018 5         52 my $res;
1019 5 50       22 if ( $self->cursor ) {
1020             $res = $self->_get_metadata(
1021             $self->{path}, $self->{param},
1022             $self->{filter}, $self->{select}
1023 5         39 );
1024             }
1025 5         45 my $last_start = $self->page_start_at;
1026            
1027             #as long as the count of items returned is equal to ->rows
1028             #there should be a next page to ask for: increment page_start_at to page_start_at + row
1029 5 50 33     23 if ( $last_start && $self->{last_page_items_count} >= $self->rows ) {
1030 0         0 $self->page_start_at( $last_start + $self->rows );
1031 0         0 $res = $self->_get_page_metadata( $self->{path}, $self->{param} );
1032             }
1033 5         62 return $res;
1034             }
1035            
1036             =head2 C<$cr-Eagencies_from_dois( $dois_array_ref )>
1037            
1038             Retrieve the Registration agency (CrossRef, mEdra ...) using an array ref of article doi.
1039             L
1040            
1041             =cut
1042            
1043             sub agencies_from_dois {
1044 0     0 1 0 my ( $self, $dois_ar ) = @_;
1045 0         0 my @results;
1046            
1047             # die Dumper $dois_ar;
1048 0         0 my $rows = $self->rows;
1049 0         0 $self->rows(0);
1050 0         0 for my $doi (@$dois_ar) {
1051            
1052             #print "looking for $doi\n";
1053 0         0 my $response =
1054             $self->_crossref_get_request( "/works/" . $doi . "/agency" );
1055 0 0       0 if ($response) {
1056 0         0 my $hr = $self->_decode_json( $response->responseContent );
1057            
1058             # my @items = $hr->{message}->{items};
1059 0         0 my $res = $self->_display_data($hr);
1060 0 0       0 return $res if ($self->spit_raw_data);
1061 0         0 push @results, $res;
1062            
1063             }
1064            
1065             }
1066 0         0 $self->rows($rows);
1067            
1068 0         0 return \@results;
1069             }
1070            
1071             =head2 C<$cr-Efunders_from_location( $a_location_name )>
1072            
1073             Retrieve the funder from a country. Problem is that there is no way of having a list of country name used.
1074             These locations has been succefully tested: United Kingdom, Germany, Japan, Morocco, Switzerland, France.
1075            
1076             =cut
1077            
1078             sub funders_from_location {
1079 0     0 1 0 my ( $self, $loc ) = @_;
1080 0 0       0 croak "funders_from_location : need location" unless $loc;
1081 0         0 my $rows = $self->rows;
1082            
1083             #$self->rows(0);
1084 0         0 my $data;
1085             my @params;
1086 0         0 push @params, "location:" . uri_escape($loc);
1087 0         0 $self->page_start_at(1);
1088 0         0 $self->{path} = "/funders";
1089 0         0 $self->{param} = \@params;
1090 0         0 $self->{select} = undef;
1091 0         0 $self->_get_page_metadata( "/funders", \@params );
1092            
1093             #$self->rows($rows);
1094             #return $data;
1095             }
1096            
1097             sub _set_cursor {
1098 7     7   46 my ( $self, $msg_hr, $n_items ) = @_;
1099 7         57 my %msg = %$msg_hr;
1100 7 100 66     190 if ( exists $msg{'next-cursor'} && $n_items >= $self->rows ) {
1101            
1102             # print "_set_cursor: ", uri_escape( $msg{'next-cursor'} ), "\n";
1103 6         92 $self->cursor( uri_escape( $msg{'next-cursor'} ) );
1104             }
1105             else {
1106             # print "_set_cursor: undef\n";
1107 1         7 $self->cursor(undef);
1108             }
1109             }
1110            
1111             sub _decode_json {
1112 9     9   274 my ( $self, $json ) = @_;
1113 9         181 my $data = $self->decoder->decode($json);
1114 9         2338 return $data;
1115            
1116             }
1117            
1118             package REST::Client::CrossRef::Unfolder;
1119            
1120             #use Data::Dumper;
1121 1     1   4644 use Carp;
  1         4  
  1         89  
1122 1     1   8 use Log::Any;
  1         2  
  1         18  
1123            
1124             sub new {
1125 8     8   29 my ($class) = shift;
1126 8         78 my $self = { logger => Log::Any->get_logger( category => "unfolder" ), };
1127 8         1072 return bless $self, $class;
1128            
1129             }
1130            
1131             sub log {
1132 885     885   1251 my $self = shift;
1133 885         2661 return $self->{logger};
1134             }
1135            
1136             # This setting of the array ref could be removed since the ordering in display_data
1137             # also remove the keys that are not wanted. But the hash builded is smaller
1138             # with adding only the key that are needed.
1139             sub set_keys_to_keep {
1140 2     2   4 my ( $self, $ar_ref ) = @_;
1141 2         11 $self->{keys_to_keep} = $ar_ref;
1142            
1143             }
1144            
1145             sub _unfold_hash {
1146 40     40   74 my ( $self, $raw_hr, $key_ar, $result_hr ) = @_;
1147            
1148 40 100       60 $self->log->debug( "unfold_hash1: ",
1149             ( $result_hr ? scalar %$result_hr : 0 ) );
1150 40         196 for my $k ( keys %$raw_hr ) {
1151            
1152             # $self->log->debug( "key: ", $k );
1153            
1154 214         327 push @$key_ar, $k;
1155            
1156 214 100       497 if ( ref $raw_hr->{$k} eq "HASH" ) {
    100          
1157            
1158             $result_hr =
1159 24         50 $self->_unfold_hash( $raw_hr->{$k}, $key_ar, $result_hr );
1160            
1161 24 50       38 $self->log->debug( "1 size ",
1162             $result_hr ? scalar %$result_hr : 0 );
1163             }
1164             elsif ( ref $raw_hr->{$k} eq "ARRAY" ) {
1165             $result_hr =
1166 30         81 $self->_unfold_array( $raw_hr->{$k}, $key_ar, $result_hr );
1167            
1168 30 50       52 $self->log->debug( "2 size ",
1169             $result_hr ? scalar %$result_hr : 0 );
1170            
1171             $result_hr->{ $key_ar->[$#$key_ar] } =~ s/,\s$//
1172 30 100       133 if ( defined $result_hr->{ $key_ar->[$#$key_ar] } );
1173            
1174             }
1175            
1176             else {
1177            
1178             $self->log->debug( "ref: ", ref $raw_hr->{$k} )
1179 160 100       299 if ( ref $raw_hr->{$k} );
1180 160         382 my $key = join( "/", @$key_ar );
1181            
1182 160 100 66     480 if ( defined $self->{keys_to_keep}
1183             && defined $self->{keys_to_keep}->{$key} )
1184             {
1185 1         3 $result_hr->{$key} = $raw_hr->{$k}
1186            
1187             }
1188             else {
1189             $self->log->debug( "key : ", $key, " value: ",
1190 159         263 $raw_hr->{$k} );
1191 159         619 $result_hr->{$key} = $raw_hr->{$k};
1192             }
1193            
1194             }
1195            
1196 214         439 my $tmp = pop @$key_ar;
1197            
1198             }
1199            
1200 40 50       76 $self->log->debug( "_unfold_hash3: ",
1201             $result_hr ? scalar(%$result_hr) : 0 );
1202 40         127 return $result_hr;
1203             }
1204            
1205             sub _unfold_array {
1206 168     168   349 my ( $self, $ar, $key_ar, $res_hr ) = @_;
1207            
1208 168 100       316 $self->log->debug( "_unfold_array0: ", $res_hr ? scalar(%$res_hr) : 0 );
1209 168         571 my $last_key = join( "/", @{$key_ar} );
  168         397  
1210 168         340 my $key = $key_ar->[$#$key_ar];
1211            
1212 168         327 $self->log->debug( "_unfold array1 key: ", $key );
1213 168 100       595 if ( $key eq "author" ) {
1214 1         6 my @first;
1215             my @groups;
1216 1         0 my $first;
1217 1         0 my @all;
1218 1         3 for my $aut (@$ar) {
1219 2 100       6 if ( $aut->{sequence} eq 'first' ) {
1220 1 50       3 if ( $aut->{family} ) {
    0          
1221             $first =
1222             "\n"
1223             . $aut->{family}
1224             . (
1225             defined $aut->{given} ? ", " . $aut->{given} : " " )
1226 1 50       9 . $self->_unfold_affiliation( $aut->{affiliation} );
1227 1         3 push @first, $first;
1228             }
1229             elsif ( $aut->{name} ) {
1230             $first = "\n"
1231             . $aut->{name}
1232 0         0 . $self->_unfold_affiliation( $aut->{affiliation} );
1233 0         0 push @groups, $first;
1234            
1235             }
1236            
1237             }
1238             else {
1239 1 50       4 if ( $aut->{family} ) {
    0          
1240             push @all,
1241             "\n"
1242             . $aut->{family}
1243             . (
1244             defined $aut->{given} ? ", " . $aut->{given} : " " )
1245 1 50       7 . $self->_unfold_affiliation( $aut->{affiliation} );
1246             }
1247             elsif ( $aut->{name} ) {
1248             push @groups,
1249             "\n"
1250             . $aut->{name}
1251 0         0 . $self->_unfold_affiliation( $aut->{affiliation} );
1252            
1253             }
1254             }
1255            
1256             }
1257            
1258 1         3 unshift @all, @first;
1259 1         2 unshift @all, @groups;
1260 1         5 $res_hr->{$key} = join( "", @all );
1261            
1262             }
1263            
1264             else {
1265            
1266 167         309 for my $val (@$ar) {
1267            
1268 208 100       524 if ( ref $val eq "HASH" ) {
    100          
1269 14         37 $res_hr = $self->_unfold_hash( $val, $key_ar, $res_hr );
1270 14         44 my $last = $#$key_ar;
1271             $res_hr->{ $key_ar->[$last] } =~ s/,\s$//
1272 14 50       36 if ( defined $res_hr->{ $key_ar->[$last] } );
1273            
1274 14 50       23 $self->log->debug( "_unfold_array2: ",
1275             $res_hr ? scalar(%$res_hr) : 0 );
1276             }
1277             elsif ( ref $val eq "ARRAY" ) {
1278 48         125 $res_hr = $self->_unfold_array( $val, $key_ar, $res_hr );
1279            
1280 48 50       100 $self->log->debug( "_unfold_array3: ",
1281             $res_hr ? scalar(%$res_hr) : 0 );
1282            
1283             }
1284             else {
1285            
1286 146 100 100     416 if ( defined $self->{keys_to_keep}
1287             && defined $self->{keys_to_keep}->{$last_key} )
1288             {
1289 2 50       5 if ( defined $val ) {
1290 2         7 $res_hr->{$last_key} .= $val . ", ";
1291             }
1292             else {
1293 0         0 $res_hr->{$last_key} = "";
1294             }
1295            
1296             }
1297             else {
1298 144         477 $res_hr->{$last_key} .= $val;
1299             }
1300            
1301             }
1302             } #for
1303            
1304             }
1305            
1306 168 50       392 $self->log->debug( "_unfold_array4: ", $res_hr ? scalar(%$res_hr) : 0 );
1307 168         760 return $res_hr;
1308             }
1309            
1310             sub _unfold_affiliation {
1311 2     2   6 my ( $self, $ar ) = @_;
1312 2         2 my $line = ";";
1313 2         3 my @aff;
1314 2         5 for my $hr (@$ar) {
1315            
1316             # my @k = keys %$hr;
1317 0         0 my @aff = values %$hr;
1318 0         0 $aff[0] =~ s/\r/ /g;
1319 0         0 $line .= " " . $aff[0];
1320             }
1321            
1322 2         6 return $line;
1323             }
1324            
1325             =head1 INSTALLATION
1326            
1327             To install this module type the following:
1328             perl Makefile.PL
1329             make
1330             make test
1331             make install
1332            
1333             On windows use nmake or dmake instead of make.
1334            
1335             =head1 DEPENDENCIES
1336            
1337             The following modules are required in order to use this one
1338            
1339             Moo => 2,
1340             JSON => 2.90,
1341             URI::Escape => 3.31,
1342             REST::Client => 273,
1343             Log::Any => 1.049,
1344             HTTP::Cache::Transparent => 1.4,
1345             Carp => 1.40,
1346             JSON::Path => 0.420
1347            
1348             =head1 BUGS
1349            
1350             See below.
1351            
1352             =head1 SUPPORT
1353            
1354             Any questions or problems can be posted to me (rappazf) on my gmail account.
1355            
1356             The current state of the source can be extract using Mercurial from
1357             L
1358            
1359             =head1 AUTHOR
1360            
1361             F. Rappaz
1362             CPAN ID: RAPPAZF
1363            
1364             =head1 COPYRIGHT
1365            
1366             This program is free software; you can redistribute
1367             it and/or modify it under the same terms as Perl itself.
1368            
1369             The full text of the license can be found in the
1370             LICENSE file included with this module.
1371            
1372             =head1 SEE ALSO
1373            
1374             L Catmandu is a toolframe, *nix oriented.
1375            
1376             L Import data from CrossRef using the CrossRef search, not the REST Api, and convert the XML result into something simpler.
1377            
1378             =cut
1379            
1380             1;
1381