File Coverage

blib/lib/WWW/Zotero.pm
Criterion Covered Total %
statement 27 224 12.0
branch 0 106 0.0
condition 0 18 0.0
subroutine 9 39 23.0
pod 22 24 91.6
total 58 411 14.1


line stmt bran cond sub pod time code
1             package WWW::Zotero;
2              
3             =pod
4              
5             =head1 NAME
6              
7             WWW::Zotero - Perl interface to the Zotero API
8              
9             =head1 SYNOPSIS
10              
11             use WWW::Zotero;
12              
13             my $client = WWW::Zotero->new;
14             my $client = WWW::Zotero->new(key => 'API-KEY');
15              
16             my $data = $client->itemTypes();
17            
18             for my $item (@$data) {
19             print "%s\n" , $item->itemType;
20             }
21              
22             my $data = $client->itemFields();
23             my $data = $client->itemTypeFields('book');
24             my $data = $client->itemTypeCreatorTypes('book');
25             my $data = $client->creatorFields();
26             my $data = $client->itemTemplate('book');
27             my $key = $client->keyPermissions();
28             my $groups = $client->userGroups($userID);
29              
30             my $data = $client->listItems(user => '475425', limit => 5);
31             my $data = $client->listItems(user => '475425', format => 'atom');
32             my $generator = $client->listItems(user => '475425', generator => 1);
33              
34             while (my $item = $generator->()) {
35             print "%s\n" , $item->{title};
36             }
37              
38             my $data = $client->listItemsTop(user => '475425', limit => 5);
39             my $data = $client->listItemsTrash(user => '475425');
40             my $data = $client->getItem(user => '475425', itemKey => 'TTJFTW87');
41             my $data = $client->getItemTags(user => '475425', itemKey => 'X42A7DEE');
42             my $data = $client->listTags(user => '475425');
43             my $data = $client->listTags(user => '475425', tag => 'Biography');
44             my $data = $client->listCollections(user => '475425');
45             my $data = $client->listCollectionsTop(user => '475425');
46             my $data = $client->getCollection(user => '475425', collectionKey => 'A5G9W6AX');
47             my $data = $client->listSubCollections(user => '475425', collectionKey => 'QM6T3KHX');
48             my $data = $client->listCollectionItems(user => '475425', collectionKey => 'QM6T3KHX');
49             my $data = $client->listCollectionItemsTop(user => '475425', collectionKey => 'QM6T3KHX');
50             my $data = $client->listCollectionItemsTags(user => '475425', collectionKey => 'QM6T3KHX');
51             my $data = $client->listSearches(user => '475425');
52              
53             =cut
54              
55 1     1   20197 use Moo;
  1         13139  
  1         6  
56 1     1   2428 use JSON;
  1         15421  
  1         6  
57 1     1   806 use URI::Escape;
  1         1144  
  1         57  
58 1     1   706 use REST::Client;
  1         42917  
  1         32  
59 1     1   860 use Data::Dumper;
  1         6687  
  1         60  
60 1     1   696 use POSIX qw(strftime);
  1         6273  
  1         5  
61 1     1   1207 use Carp;
  1         2  
  1         46  
62 1     1   662 use Log::Any ();
  1         4485  
  1         21  
63 1     1   5 use feature 'state';
  1         2  
  1         2953  
64              
65             our $VERSION = '0.03';
66              
67             =head1 CONFIGURATION
68              
69             =over 4
70              
71             =item baseurl
72              
73             The base URL for all API requests. Default 'https://api.zotero.org'.
74              
75             =item version
76              
77             The API version. Default '3'.
78              
79             =item key
80              
81             The API key which can be requested via https://api.zotero.org.
82              
83             =item modified_since
84              
85             Include a UNIX time to be used in a If-Modified-Since header to allow for caching
86             of results by your application.
87              
88             =back
89              
90             =cut
91             has baseurl => (is => 'ro' , default => sub { 'https://api.zotero.org' });
92             has modified_since => (is => 'ro');
93             has version => (is => 'ro' , default => sub { '3'});
94             has key => (is => 'ro');
95             has code => (is => 'rw');
96             has sleep => (is => 'rw' , default => sub { 0 });
97             has log => (is => 'lazy');
98             has client => (is => 'lazy');
99              
100             sub _build_client {
101 0     0     my ($self) = @_;
102 0           my $client = REST::Client->new();
103              
104 0           $self->log->debug("< Zotero-API-Version: " . $self->version);
105 0           $client->addHeader('Zotero-API-Version', $self->version);
106              
107 0 0         if (defined $self->key) {
108 0           my $authorization = 'Bearer ' . $self->key;
109 0           $self->log->debug("< Authorization: " . $authorization);
110 0           $client->addHeader('Authorization', $authorization);
111             }
112              
113 0 0         if (defined $self->modified_since) {
114 0           my $date = strftime "%a, %d %b %Y %H:%M:%S GMT" , gmtime($self->modified_since);
115 0           $self->log->debug("< If-Modified-Since: " . $date);
116 0           $client->addHeader('If-Modified-Since',$date);
117             }
118              
119 0           $client;
120             }
121              
122             sub _build_log {
123 0     0     my ($self) = @_;
124 0           Log::Any->get_logger(category => ref($self));
125             }
126              
127             sub _zotero_get_request {
128 0     0     my ($self,$path,%param) = @_;
129              
130 0           my $url = sprintf "%s%s" , $self->baseurl, $path;
131              
132 0           my @params = ();
133 0           for my $name (keys %param) {
134 0           my $value = $param{$name};
135 0           push @params , uri_escape($name) . "=" . uri_escape($value);
136             }
137              
138 0 0         $url .= '?' . join("&",@params) if @params > 0;
139              
140             # The server asked us to sleep..
141 0 0         if ($self->sleep > 0) {
142 0           $self->log->debug("sleeping: " . $self->sleep . " seconds");
143 0           sleep $self->sleep;
144 0           $self->sleep(0)
145             }
146              
147 0           $self->log->debug("requesting: $url");
148 0           my $response = $self->client->GET($url);
149              
150 0   0       my $backoff = $response->responseHeader('Backoff') // 0;
151 0   0       my $retryAfter = $response->responseHeader('Retry-After') // 0;
152 0           my $code = $response->responseCode();
153              
154 0           $self->log->debug("> Code: $code");
155 0           $self->log->debug("> Backoff: $backoff");
156 0           $self->log->debug("> Retry-After: $retryAfter");
157              
158 0 0 0       if ($backoff > 0) {
    0          
159 0           $self->sleep($backoff);
160             }
161             elsif ($code eq '429' || $code eq '503') {
162 0   0       $self->sleep($retryAfter // 60);
163 0           return undef;
164             }
165            
166 0           $self->log->debug("> Content: " . $response->responseContent);
167              
168 0           $self->code($code);
169              
170 0 0         return undef unless $code eq '200';
171              
172 0           $response;
173             }
174              
175             =head1 METHODS
176              
177             =head2 itemTypes()
178              
179             Get all item types. Returns a Perl array.
180              
181             =cut
182             sub itemTypes {
183 0     0 1   my ($self) = @_;
184              
185 0           my $response = $self->_zotero_get_request('/itemTypes');
186              
187 0 0         return undef unless $response;
188              
189 0           decode_json $response->responseContent;
190             }
191              
192             =head2 itemTypes()
193              
194             Get all item fields. Returns a Perl array.
195              
196             =cut
197             sub itemFields {
198 0     0 0   my ($self) = @_;
199              
200 0           my $response = $self->_zotero_get_request('/itemFields');
201              
202 0 0         return undef unless $response;
203              
204 0           decode_json $response->responseContent;
205             }
206              
207             =head2 itemTypes($type)
208              
209             Get all valid fields for an item type. Returns a Perl array.
210              
211             =cut
212             sub itemTypeFields {
213 0     0 0   my ($self,$itemType) = @_;
214              
215 0 0         croak "itemTypeFields: need itemType" unless defined $itemType;
216              
217 0           my $response = $self->_zotero_get_request('/itemTypeFields', itemType => $itemType);
218              
219 0 0         return undef unless $response;
220              
221 0           decode_json $response->responseContent;
222             }
223              
224             =head2 itemTypeCreatorTypes($type)
225              
226             Get valid creator types for an item type. Returns a Perl array.
227              
228             =cut
229             sub itemTypeCreatorTypes {
230 0     0 1   my ($self,$itemType) = @_;
231              
232 0 0         croak "itemTypeCreatorTypes: need itemType" unless defined $itemType;
233              
234 0           my $response = $self->_zotero_get_request('/itemTypeCreatorTypes', itemType => $itemType);
235              
236 0 0         return undef unless $response;
237              
238 0           decode_json $response->responseContent;
239             }
240              
241             =head2 creatorFields()
242              
243             Get localized creator fields. Returns a Perl array.
244              
245             =cut
246             sub creatorFields {
247 0     0 1   my ($self) = @_;
248              
249 0           my $response = $self->_zotero_get_request('/creatorFields');
250              
251 0 0         return undef unless $response;
252              
253 0           decode_json $response->responseContent;
254             }
255              
256             =head2 itemTemplate($type)
257              
258             Get a template for a new item. Returns a Perl hash.
259              
260             =cut
261             sub itemTemplate {
262 0     0 1   my ($self,$itemType) = @_;
263              
264 0 0         croak "itemTemplate: need itemType" unless defined $itemType;
265              
266 0           my $response = $self->_zotero_get_request('/items/new', itemType => $itemType);
267              
268 0 0         return undef unless $response;
269              
270 0           decode_json $response->responseContent;
271             }
272              
273             =head2 keyPermissions($key)
274              
275             Return the userID and premissions for the given API key.
276              
277             =cut
278             sub keyPermissions {
279 0     0 1   my ($self,$key) = @_;
280              
281 0 0         $key = $self->key unless defined $key;
282              
283 0 0         croak "keyPermissions: need key" unless defined $key;
284              
285 0           my $response = $self->_zotero_get_request("/keys/$key");
286              
287 0 0         return undef unless $response;
288              
289 0           decode_json $response->responseContent;
290             }
291              
292             =head2 userGroups($userID)
293              
294             Return an array of the set of groups the current API key as access to.
295              
296             =cut
297             sub userGroups {
298 0     0 1   my ($self,$userID) = @_;
299              
300 0 0         croak "userGroups: need userID" unless defined $userID;
301              
302 0           my $response = $self->_zotero_get_request("/users/$userID/groups");
303              
304 0 0         return undef unless $response;
305            
306 0           decode_json $response->responseContent;
307             }
308              
309             =head2 listItems(user => $userID, %options)
310              
311             =head2 listItems(group => $groupID, %options)
312              
313             List all items for a user or ar group. Optionally provide a list of options:
314              
315             sort - dateAdded, dateModified, title, creator, type, date, publisher,
316             publicationTitle, journalAbbreviation, language, accessDate,
317             libraryCatalog, callNumber, rights, addedBy, numItems (default dateModified)
318             direction - asc, desc
319             limit - integer 1-100* (default 25)
320             start - integer
321             format - perl, atom, bib, json, keys, versions , bibtex , bookmarks,
322             coins, csljson, mods, refer, rdf_bibliontology , rdf_dc ,
323             rdf_zotero, ris , tei , wikipedia (default perl)
324              
325             when format => 'json'
326              
327             include - bib, data
328              
329             when format => 'atom'
330            
331             content - bib, html, json
332              
333             when format => 'bib' or content => 'bib'
334              
335             style - chicago-note-bibliography, apa, ... (see: https://www.zotero.org/styles/)
336              
337              
338             itemKey - A comma-separated list of item keys. Valid only for item requests. Up to
339             50 items can be specified in a single request.
340             itemType - Item type search
341             q - quick search
342             qmode - titleCreatorYear, everything
343             since - integer
344             tag - Tag search
345              
346             See: https://www.zotero.org/support/dev/web_api/v3/basics#user_and_group_library_urls
347             for the search syntax.
348              
349             Returns a Perl HASH containing the total number of hits plus the results:
350            
351             {
352             total => '132',
353             results =>
354             }
355              
356             =head2 listItems(user => $userID | group => $groupID, generator => 1 , %options)
357              
358             Same as listItems but this return a generator for every record found. Use this
359             method to sequentially read the complete resultset. E.g.
360              
361             my $generator = $self->listItems(user => '231231', generator);
362              
363             while (my $record = $generator->()) {
364             printf "%s\n" , $record->{title};
365             }
366              
367             The format is implicit 'perl' in this case.
368              
369             =cut
370             sub listItems {
371 0     0 1   my ($self,%options) = @_;
372              
373 0           $self->_listItems(%options, path => 'items');
374             }
375              
376             sub _listItems {
377 0     0     my ($self,%options) = @_;
378              
379 0           my $userID = $options{user};
380 0           my $groupID = $options{group};
381              
382 0 0 0       croak "listItems: need user or group" unless defined $userID || defined $groupID;
383              
384 0 0         my $id = defined $userID ? $userID : $groupID;
385 0 0         my $type = defined $userID ? 'users' : 'groups';
386            
387 0           my $generator = $options{generator};
388 0           my $path = $options{path};
389            
390 0           delete $options{generator};
391 0           delete $options{path};
392 0           delete $options{user};
393 0           delete $options{group};
394 0 0 0       delete $options{format} if exists $options{format} && $options{format} eq 'perl';
395              
396 0 0         $options{limit} = 25 unless defined $options{limit};
397              
398 0 0         if ($generator) {
399 0           delete $options{format};
400 0 0         $options{start} = 0 unless defined $options{start};
401              
402             return sub {
403 0     0     state $response = $self->_listItems_request("/$type/$id/$path", %options);
404 0           state $idx = 0;
405              
406 0 0         return undef unless defined $response;
407 0 0         return undef if $response->{total} == 0;
408 0 0         return undef if $options{start} + $idx + 1 >= $response->{total};
409              
410 0 0         unless (defined $response->{results}->[$idx]) {
411 0           $options{start} += $options{limit};
412 0           $response = $self->_listItems_request("/$type/$id/$path", %options);
413 0           $idx = 0;
414             }
415              
416 0 0         return undef unless defined $response;
417              
418 0           my $doc = $response->{results}->[$idx];
419 0           my $id = $doc->{key};
420              
421 0           $idx++;
422              
423 0           { _id => $id , %$doc };
424 0           };
425             }
426             else {
427 0           return $self->_listItems_request("/$type/$id/$path", %options);
428             }
429             }
430              
431             sub _listItems_request {
432 0     0     my ($self,$path,%options) = @_;
433 0           my $response = $self->_zotero_get_request($path, %options);
434              
435 0 0         return undef unless defined $response;
436              
437 0           my $total = $response->responseHeader('Total-Results');
438 0           my $link = $response->responseHeader('Link');
439              
440 0 0         $self->log->debug("> Total-Results: $total") if defined $total;
441 0 0         $self->log->debug("> Link: $link") if defined $link;
442              
443 0           my $results = $response->responseContent;
444              
445 0 0         return undef unless $results;
446              
447 0 0 0       if (! defined $options{format} || $options{format} eq 'perl') {
448 0           $results = decode_json $results;
449             }
450              
451             return {
452 0           total => $total,
453             results => $results
454             };
455             }
456              
457             =head2 listItemsTop(user => $userID | group => $groupID, %options)
458              
459             The set of all top-level items in the library, excluding trashed items.
460              
461             See 'listItems(...)' functions above for all the execution options.
462              
463             =cut
464             sub listItemsTop {
465 0     0 1   my ($self,%options) = @_;
466              
467 0           $self->_listItems(%options, path => 'items/top');
468             }
469              
470             =head2 listItemsTrash(user => $userID | group => $groupID, %options)
471              
472             The set of items in the trash.
473              
474             See 'listItems(...)' functions above for all the execution options.
475              
476             =cut
477             sub listItemsTrash {
478 0     0 1   my ($self,%options) = @_;
479              
480 0           $self->_listItems(%options, path => 'items/trash');
481             }
482              
483             =head2 getItem(itemKey => ... , user => $userID | group => $groupID, %options)
484              
485             A specific item in the library.
486              
487             See 'listItems(...)' functions above for all the execution options.
488              
489             Returns the item if found.
490              
491             =cut
492             sub getItem {
493 0     0 1   my ($self,%options) = @_;
494              
495 0           my $key = $options{itemKey};
496              
497 0 0         croak "getItem: need itemKey" unless defined $key;
498              
499 0           delete $options{itemKey};
500              
501 0           my $result = $self->_listItems(%options, path => "items/$key");
502              
503 0 0         return undef unless defined $result;
504              
505 0           $result->{results};
506             }
507              
508             =head2 getItemChildren(itemKey => ... , user => $userID | group => $groupID, %options)
509              
510             The set of all child items under a specific item.
511              
512             See 'listItems(...)' functions above for all the execution options.
513              
514             Returns the children if found.
515              
516             =cut
517             sub getItemChildren {
518 0     0 1   my ($self,%options) = @_;
519              
520 0           my $key = $options{itemKey};
521              
522 0 0         croak "getItem: need itemKey" unless defined $key;
523              
524 0           delete $options{itemKey};
525              
526 0           my $result = $self->_listItems(%options, path => "items/$key/children");
527              
528 0 0         return undef unless defined $result;
529              
530 0           $result->{results};
531             }
532              
533             =head2 getItemTags(itemKey => ... , user => $userID | group => $groupID, %options)
534              
535             The set of all tags associated with a specific item.
536              
537             See 'listItems(...)' functions above for all the execution options.
538              
539             Returns the tags if found.
540              
541             =cut
542             sub getItemTags {
543 0     0 1   my ($self,%options) = @_;
544              
545 0           my $key = $options{itemKey};
546              
547 0 0         croak "getItem: need itemKey" unless defined $key;
548              
549 0           delete $options{itemKey};
550              
551 0           my $result = $self->_listItems(%options, path => "items/$key/tags");
552              
553 0 0         return undef unless defined $result;
554              
555 0           $result->{results};
556             }
557              
558             =head2 listTags(user => $userID | group => $groupID, [tag => $name] , %options)
559              
560             The set of tags (i.e., of all types) matching a specific name.
561              
562             See 'listItems(...)' functions above for all the execution options.
563              
564             Returns the list of tags.
565              
566             =cut
567             sub listTags {
568 0     0 1   my ($self,%options) = @_;
569              
570 0           my $tag = $options{tag};
571              
572 0           delete $options{tag};
573              
574 0 0         my $path = defined $tag ? "tags/" . uri_escape($tag) : "tags";
575              
576 0           $self->_listItems(%options, path => $path);
577             }
578              
579             =head2 listCollections(user => $userID | group => $groupID , %options)
580              
581             The set of all collections in the library.
582              
583             See 'listItems(...)' functions above for all the execution options.
584              
585             Returns the list of collections.
586              
587             =cut
588             sub listCollections {
589 0     0 1   my ($self,%options) = @_;
590              
591 0           $self->_listItems(%options, path => "/collections");
592             }
593              
594             =head2 listCollectionsTop(user => $userID | group => $groupID , %options)
595              
596             The set of all top-level collections in the library.
597              
598             See 'listItems(...)' functions above for all the execution options.
599              
600             Returns the list of collections.
601              
602             =cut
603             sub listCollectionsTop {
604 0     0 1   my ($self,%options) = @_;
605              
606 0           $self->_listItems(%options, path => "collections/top");
607             }
608              
609             =head2 getCollection(collectionKey => ... , user => $userID | group => $groupID, %options)
610              
611             A specific item in the library.
612              
613             See 'listItems(...)' functions above for all the execution options.
614              
615             Returns the collection if found.
616              
617             =cut
618             sub getCollection {
619 0     0 1   my ($self,%options) = @_;
620              
621 0           my $key = $options{collectionKey};
622              
623 0 0         croak "getCollection: need collectionKey" unless defined $key;
624              
625 0           delete $options{collectionKey};
626              
627 0           my $result = $self->_listItems(%options, path => "collections/$key");
628              
629 0 0         return undef unless defined $result;
630              
631 0           $result->{results};
632             }
633              
634             =head2 listSubCollections(collectionKey => ...., user => $userID | group => $groupID , %options)
635              
636             The set of subcollections within a specific collection in the library.
637              
638             See 'listItems(...)' functions above for all the execution options.
639              
640             Returns the list of (sub)collections.
641              
642             =cut
643             sub listSubCollections {
644 0     0 1   my ($self,%options) = @_;
645              
646 0           my $key = $options{collectionKey};
647              
648 0 0         croak "listSubCollections: need collectionKey" unless defined $key;
649              
650 0           delete $options{collectionKey};
651              
652 0           $self->_listItems(%options, path => "collections/$key/collections");
653             }
654              
655             =head2 listCollectionItems(collectionKey => ...., user => $userID | group => $groupID , %options)
656              
657             The set of all items within a specific collection in the library.
658              
659             See 'listItems(...)' functions above for all the execution options.
660              
661             Returns the list of items.
662              
663             =cut
664             sub listCollectionItems {
665 0     0 1   my ($self,%options) = @_;
666              
667 0           my $key = $options{collectionKey};
668              
669 0 0         croak "listCollectionItems: need collectionKey" unless defined $key;
670              
671 0           delete $options{collectionKey};
672              
673 0           $self->_listItems(%options, path => "collections/$key/items");
674             }
675              
676             =head2 listCollectionItemsTop(collectionKey => ...., user => $userID | group => $groupID , %options)
677              
678             The set of top-level items within a specific collection in the library.
679              
680             See 'listItems(...)' functions above for all the execution options.
681              
682             Returns the list of items.
683              
684             =cut
685             sub listCollectionItemsTop {
686 0     0 1   my ($self,%options) = @_;
687              
688 0           my $key = $options{collectionKey};
689              
690 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
691              
692 0           delete $options{collectionKey};
693              
694 0           $self->_listItems(%options, path => "collections/$key/items/top");
695             }
696              
697             =head2 listCollectionItemsTags(collectionKey => ...., user => $userID | group => $groupID , %options)
698              
699             The set of tags within a specific collection in the library.
700              
701             See 'listItems(...)' functions above for all the execution options.
702              
703             Returns the list of items.
704              
705             =cut
706             sub listCollectionItemsTags {
707 0     0 1   my ($self,%options) = @_;
708              
709 0           my $key = $options{collectionKey};
710              
711 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
712              
713 0           delete $options{collectionKey};
714              
715 0           $self->_listItems(%options, path => "collections/$key/tags");
716             }
717              
718             =head2 listSearches(user => $userID | group => $groupID , %options)
719              
720             The set of all saved searches in the library.
721              
722             See 'listItems(...)' functions above for all the execution options.
723              
724             Returns the list of saved searches.
725              
726             =cut
727             sub listSearches {
728 0     0 1   my ($self,%options) = @_;
729              
730 0           $self->_listItems(%options, path => "searches");
731             }
732              
733             =head2 getSearch(searchKey => ... , user => $userID | group => $groupID, %options)
734              
735             A specific saved search in the library.
736              
737             See 'listItems(...)' functions above for all the execution options.
738              
739             Returns the saved search if found.
740              
741             =cut
742             sub getSearch {
743 0     0 1   my ($self,%options) = @_;
744              
745 0           my $key = $options{searchKey};
746              
747 0 0         croak "getSearch: need searchKey" unless defined $key;
748              
749 0           delete $options{searchKey};
750              
751 0           my $result = $self->_listItems(%options, path => "search/$key");
752              
753 0 0         return undef unless defined $result;
754              
755 0           $result->{results};
756             }
757              
758             =head1 AUTHOR
759              
760             Patrick Hochstenbach, C<< >>
761              
762             =head1 LICENSE AND COPYRIGHT
763              
764             Copyright 2015 Patrick Hochstenbach
765              
766             This program is free software; you can redistribute it and/or modify it
767             under the terms of either: the GNU General Public License as published
768             by the Free Software Foundation; or the Artistic License.
769              
770             See http://dev.perl.org/licenses/ for more information.
771              
772             =cut
773              
774             1;
775              
776