File Coverage

blib/lib/WWW/Zotero.pm
Criterion Covered Total %
statement 27 216 12.5
branch 0 90 0.0
condition 0 18 0.0
subroutine 9 39 23.0
pod 22 24 91.6
total 58 387 14.9


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   19897 use Moo;
  1         13729  
  1         6  
56 1     1   2428 use JSON;
  1         16229  
  1         5  
57 1     1   839 use URI::Escape;
  1         1235  
  1         61  
58 1     1   702 use REST::Client;
  1         93758  
  1         36  
59 1     1   981 use Data::Dumper;
  1         7150  
  1         80  
60 1     1   828 use POSIX qw(strftime);
  1         6490  
  1         7  
61 1     1   1401 use Carp;
  1         2  
  1         51  
62 1     1   685 use Log::Any ();
  1         42641  
  1         48  
63 1     1   15 use feature 'state';
  1         3  
  1         5346  
64              
65             our $VERSION = '0.02';
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           decode_json $response->responseContent;
188             }
189              
190             =head2 itemTypes()
191              
192             Get all item fields. Returns a Perl array.
193              
194             =cut
195             sub itemFields {
196 0     0 0   my ($self) = @_;
197              
198 0           my $response = $self->_zotero_get_request('/itemFields');
199              
200 0           decode_json $response->responseContent;
201             }
202              
203             =head2 itemTypes($type)
204              
205             Get all valid fields for an item type. Returns a Perl array.
206              
207             =cut
208             sub itemTypeFields {
209 0     0 0   my ($self,$itemType) = @_;
210              
211 0 0         croak "itemTypeFields: need itemType" unless defined $itemType;
212              
213 0           my $response = $self->_zotero_get_request('/itemTypeFields', itemType => $itemType);
214              
215 0           decode_json $response->responseContent;
216             }
217              
218             =head2 itemTypeCreatorTypes($type)
219              
220             Get valid creator types for an item type. Returns a Perl array.
221              
222             =cut
223             sub itemTypeCreatorTypes {
224 0     0 1   my ($self,$itemType) = @_;
225              
226 0 0         croak "itemTypeCreatorTypes: need itemType" unless defined $itemType;
227              
228 0           my $response = $self->_zotero_get_request('/itemTypeCreatorTypes', itemType => $itemType);
229              
230 0           decode_json $response->responseContent;
231             }
232              
233             =head2 creatorFields()
234              
235             Get localized creator fields. Returns a Perl array.
236              
237             =cut
238             sub creatorFields {
239 0     0 1   my ($self) = @_;
240              
241 0           my $response = $self->_zotero_get_request('/creatorFields');
242              
243 0           decode_json $response->responseContent;
244             }
245              
246             =head2 itemTemplate($type)
247              
248             Get a template for a new item. Returns a Perl hash.
249              
250             =cut
251             sub itemTemplate {
252 0     0 1   my ($self,$itemType) = @_;
253              
254 0 0         croak "itemTemplate: need itemType" unless defined $itemType;
255              
256 0           my $response = $self->_zotero_get_request('/items/new', itemType => $itemType);
257              
258 0           decode_json $response->responseContent;
259             }
260              
261             =head2 keyPermissions($key)
262              
263             Return the userID and premissions for the given API key.
264              
265             =cut
266             sub keyPermissions {
267 0     0 1   my ($self,$key) = @_;
268              
269 0 0         $key = $self->key unless defined $key;
270              
271 0 0         croak "keyPermissions: need key" unless defined $key;
272              
273 0           my $response = $self->_zotero_get_request("/keys/$key");
274              
275 0           decode_json $response->responseContent;
276             }
277              
278             =head2 userGroups($userID)
279              
280             Return an array of the set of groups the current API key as access to.
281              
282             =cut
283             sub userGroups {
284 0     0 1   my ($self,$userID) = @_;
285              
286 0 0         croak "userGroups: need userID" unless defined $userID;
287              
288 0           my $response = $self->_zotero_get_request("/users/$userID/groups");
289              
290 0           decode_json $response->responseContent;
291             }
292              
293             =head2 listItems(user => $userID, %options)
294              
295             =head2 listItems(group => $groupID, %options)
296              
297             List all items for a user or ar group. Optionally provide a list of options:
298              
299             sort - dateAdded, dateModified, title, creator, type, date, publisher,
300             publicationTitle, journalAbbreviation, language, accessDate,
301             libraryCatalog, callNumber, rights, addedBy, numItems (default dateModified)
302             direction - asc, desc
303             limit - integer 1-100* (default 25)
304             start - integer
305             format - perl, atom, bib, json, keys, versions , bibtex , bookmarks,
306             coins, csljson, mods, refer, rdf_bibliontology , rdf_dc ,
307             rdf_zotero, ris , tei , wikipedia (default perl)
308              
309             when format => 'json'
310              
311             include - bib, data
312              
313             when format => 'atom'
314            
315             content - bib, html, json
316              
317             when format => 'bib' or content => 'bib'
318              
319             style - chicago-note-bibliography, apa, ... (see: https://www.zotero.org/styles/)
320              
321              
322             itemKey - A comma-separated list of item keys. Valid only for item requests. Up to
323             50 items can be specified in a single request.
324             itemType - Item type search
325             q - quick search
326             qmode - titleCreatorYear, everything
327             since - integer
328             tag - Tag search
329              
330             See: https://www.zotero.org/support/dev/web_api/v3/basics#user_and_group_library_urls
331             for the search syntax.
332              
333             Returns a Perl HASH containing the total number of hits plus the results:
334            
335             {
336             total => '132',
337             results =>
338             }
339              
340             =head2 listItems(user => $userID | group => $groupID, generator => 1 , %options)
341              
342             Same as listItems but this return a generator for every record found. Use this
343             method to sequentially read the complete resultset. E.g.
344              
345             my $generator = $self->listItems(user => '231231', generator);
346              
347             while (my $record = $generator->()) {
348             printf "%s\n" , $record->{title};
349             }
350              
351             The format is implicit 'perl' in this case.
352              
353             =cut
354             sub listItems {
355 0     0 1   my ($self,%options) = @_;
356              
357 0           $self->_listItems(%options, path => 'items');
358             }
359              
360             sub _listItems {
361 0     0     my ($self,%options) = @_;
362              
363 0           my $userID = $options{user};
364 0           my $groupID = $options{group};
365              
366 0 0 0       croak "listItems: need user or group" unless defined $userID || defined $groupID;
367              
368 0 0         my $id = defined $userID ? $userID : $groupID;
369 0 0         my $type = defined $userID ? 'users' : 'groups';
370            
371 0           my $generator = $options{generator};
372 0           my $path = $options{path};
373            
374 0           delete $options{generator};
375 0           delete $options{path};
376 0           delete $options{user};
377 0           delete $options{group};
378 0 0 0       delete $options{format} if exists $options{format} && $options{format} eq 'perl';
379              
380 0 0         $options{limit} = 25 unless defined $options{limit};
381              
382 0 0         if ($generator) {
383 0           delete $options{format};
384 0 0         $options{start} = 0 unless defined $options{start};
385              
386             return sub {
387 0     0     state $response = $self->_listItems_request("/$type/$id/$path", %options);
388 0           state $idx = 0;
389              
390 0 0         return undef unless defined $response;
391 0 0         return undef if $response->{total} == 0;
392 0 0         return undef if $options{start} + $idx + 1 >= $response->{total};
393              
394 0 0         unless (defined $response->{results}->[$idx]) {
395 0           $options{start} += $options{limit};
396 0           $response = $self->_listItems_request("/$type/$id/$path", %options);
397 0           $idx = 0;
398             }
399              
400 0 0         return undef unless defined $response;
401              
402 0           my $doc = $response->{results}->[$idx];
403 0           my $id = $doc->{key};
404              
405 0           $idx++;
406              
407 0           { _id => $id , %$doc };
408 0           };
409             }
410             else {
411 0           return $self->_listItems_request("/$type/$id/$path", %options);
412             }
413             }
414              
415             sub _listItems_request {
416 0     0     my ($self,$path,%options) = @_;
417 0           my $response = $self->_zotero_get_request($path, %options);
418              
419 0 0         return undef unless defined $response;
420              
421 0           my $total = $response->responseHeader('Total-Results');
422 0           my $link = $response->responseHeader('Link');
423              
424 0 0         $self->log->debug("> Total-Results: $total") if defined $total;
425 0 0         $self->log->debug("> Link: $link") if defined $link;
426              
427 0           my $results = $response->responseContent;
428              
429 0 0         return undef unless $results;
430              
431 0 0 0       if (! defined $options{format} || $options{format} eq 'perl') {
432 0           $results = decode_json $results;
433             }
434              
435             return {
436 0           total => $total,
437             results => $results
438             };
439             }
440              
441             =head2 listItemsTop(user => $userID | group => $groupID, %options)
442              
443             The set of all top-level items in the library, excluding trashed items.
444              
445             See 'listItems(...)' functions above for all the execution options.
446              
447             =cut
448             sub listItemsTop {
449 0     0 1   my ($self,%options) = @_;
450              
451 0           $self->_listItems(%options, path => 'items/top');
452             }
453              
454             =head2 listItemsTrash(user => $userID | group => $groupID, %options)
455              
456             The set of items in the trash.
457              
458             See 'listItems(...)' functions above for all the execution options.
459              
460             =cut
461             sub listItemsTrash {
462 0     0 1   my ($self,%options) = @_;
463              
464 0           $self->_listItems(%options, path => 'items/trash');
465             }
466              
467             =head2 getItem(itemKey => ... , user => $userID | group => $groupID, %options)
468              
469             A specific item in the library.
470              
471             See 'listItems(...)' functions above for all the execution options.
472              
473             Returns the item if found.
474              
475             =cut
476             sub getItem {
477 0     0 1   my ($self,%options) = @_;
478              
479 0           my $key = $options{itemKey};
480              
481 0 0         croak "getItem: need itemKey" unless defined $key;
482              
483 0           delete $options{itemKey};
484              
485 0           my $result = $self->_listItems(%options, path => "items/$key");
486              
487 0 0         return undef unless defined $result;
488              
489 0           $result->{results};
490             }
491              
492             =head2 getItemChildren(itemKey => ... , user => $userID | group => $groupID, %options)
493              
494             The set of all child items under a specific item.
495              
496             See 'listItems(...)' functions above for all the execution options.
497              
498             Returns the children if found.
499              
500             =cut
501             sub getItemChildren {
502 0     0 1   my ($self,%options) = @_;
503              
504 0           my $key = $options{itemKey};
505              
506 0 0         croak "getItem: need itemKey" unless defined $key;
507              
508 0           delete $options{itemKey};
509              
510 0           my $result = $self->_listItems(%options, path => "items/$key/children");
511              
512 0 0         return undef unless defined $result;
513              
514 0           $result->{results};
515             }
516              
517             =head2 getItemTags(itemKey => ... , user => $userID | group => $groupID, %options)
518              
519             The set of all tags associated with a specific item.
520              
521             See 'listItems(...)' functions above for all the execution options.
522              
523             Returns the tags if found.
524              
525             =cut
526             sub getItemTags {
527 0     0 1   my ($self,%options) = @_;
528              
529 0           my $key = $options{itemKey};
530              
531 0 0         croak "getItem: need itemKey" unless defined $key;
532              
533 0           delete $options{itemKey};
534              
535 0           my $result = $self->_listItems(%options, path => "items/$key/tags");
536              
537 0 0         return undef unless defined $result;
538              
539 0           $result->{results};
540             }
541              
542             =head2 listTags(user => $userID | group => $groupID, [tag => $name] , %options)
543              
544             The set of tags (i.e., of all types) matching a specific name.
545              
546             See 'listItems(...)' functions above for all the execution options.
547              
548             Returns the list of tags.
549              
550             =cut
551             sub listTags {
552 0     0 1   my ($self,%options) = @_;
553              
554 0           my $tag = $options{tag};
555              
556 0           delete $options{tag};
557              
558 0 0         my $path = defined $tag ? "tags/" . uri_escape($tag) : "tags";
559              
560 0           $self->_listItems(%options, path => $path);
561             }
562              
563             =head2 listCollections(user => $userID | group => $groupID , %options)
564              
565             The set of all collections in the library.
566              
567             See 'listItems(...)' functions above for all the execution options.
568              
569             Returns the list of collections.
570              
571             =cut
572             sub listCollections {
573 0     0 1   my ($self,%options) = @_;
574              
575 0           $self->_listItems(%options, path => "/collections");
576             }
577              
578             =head2 listCollectionsTop(user => $userID | group => $groupID , %options)
579              
580             The set of all top-level collections in the library.
581              
582             See 'listItems(...)' functions above for all the execution options.
583              
584             Returns the list of collections.
585              
586             =cut
587             sub listCollectionsTop {
588 0     0 1   my ($self,%options) = @_;
589              
590 0           $self->_listItems(%options, path => "collections/top");
591             }
592              
593             =head2 getCollection(collectionKey => ... , user => $userID | group => $groupID, %options)
594              
595             A specific item in the library.
596              
597             See 'listItems(...)' functions above for all the execution options.
598              
599             Returns the collection if found.
600              
601             =cut
602             sub getCollection {
603 0     0 1   my ($self,%options) = @_;
604              
605 0           my $key = $options{collectionKey};
606              
607 0 0         croak "getCollection: need collectionKey" unless defined $key;
608              
609 0           delete $options{collectionKey};
610              
611 0           my $result = $self->_listItems(%options, path => "collections/$key");
612              
613 0 0         return undef unless defined $result;
614              
615 0           $result->{results};
616             }
617              
618             =head2 listSubCollections(collectionKey => ...., user => $userID | group => $groupID , %options)
619              
620             The set of subcollections within a specific collection in the library.
621              
622             See 'listItems(...)' functions above for all the execution options.
623              
624             Returns the list of (sub)collections.
625              
626             =cut
627             sub listSubCollections {
628 0     0 1   my ($self,%options) = @_;
629              
630 0           my $key = $options{collectionKey};
631              
632 0 0         croak "listSubCollections: need collectionKey" unless defined $key;
633              
634 0           delete $options{collectionKey};
635              
636 0           $self->_listItems(%options, path => "collections/$key/collections");
637             }
638              
639             =head2 listCollectionItems(collectionKey => ...., user => $userID | group => $groupID , %options)
640              
641             The set of all items within a specific collection in the library.
642              
643             See 'listItems(...)' functions above for all the execution options.
644              
645             Returns the list of items.
646              
647             =cut
648             sub listCollectionItems {
649 0     0 1   my ($self,%options) = @_;
650              
651 0           my $key = $options{collectionKey};
652              
653 0 0         croak "listCollectionItems: need collectionKey" unless defined $key;
654              
655 0           delete $options{collectionKey};
656              
657 0           $self->_listItems(%options, path => "collections/$key/items");
658             }
659              
660             =head2 listCollectionItemsTop(collectionKey => ...., user => $userID | group => $groupID , %options)
661              
662             The set of top-level items within a specific collection in the library.
663              
664             See 'listItems(...)' functions above for all the execution options.
665              
666             Returns the list of items.
667              
668             =cut
669             sub listCollectionItemsTop {
670 0     0 1   my ($self,%options) = @_;
671              
672 0           my $key = $options{collectionKey};
673              
674 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
675              
676 0           delete $options{collectionKey};
677              
678 0           $self->_listItems(%options, path => "collections/$key/items/top");
679             }
680              
681             =head2 listCollectionItemsTags(collectionKey => ...., user => $userID | group => $groupID , %options)
682              
683             The set of tags within a specific collection in the library.
684              
685             See 'listItems(...)' functions above for all the execution options.
686              
687             Returns the list of items.
688              
689             =cut
690             sub listCollectionItemsTags {
691 0     0 1   my ($self,%options) = @_;
692              
693 0           my $key = $options{collectionKey};
694              
695 0 0         croak "listCollectionItemsTop: need collectionKey" unless defined $key;
696              
697 0           delete $options{collectionKey};
698              
699 0           $self->_listItems(%options, path => "collections/$key/tags");
700             }
701              
702             =head2 listSearches(user => $userID | group => $groupID , %options)
703              
704             The set of all saved searches in the library.
705              
706             See 'listItems(...)' functions above for all the execution options.
707              
708             Returns the list of saved searches.
709              
710             =cut
711             sub listSearches {
712 0     0 1   my ($self,%options) = @_;
713              
714 0           $self->_listItems(%options, path => "searches");
715             }
716              
717             =head2 getSearch(searchKey => ... , user => $userID | group => $groupID, %options)
718              
719             A specific saved search in the library.
720              
721             See 'listItems(...)' functions above for all the execution options.
722              
723             Returns the saved search if found.
724              
725             =cut
726             sub getSearch {
727 0     0 1   my ($self,%options) = @_;
728              
729 0           my $key = $options{searchKey};
730              
731 0 0         croak "getSearch: need searchKey" unless defined $key;
732              
733 0           delete $options{searchKey};
734              
735 0           my $result = $self->_listItems(%options, path => "search/$key");
736              
737 0 0         return undef unless defined $result;
738              
739 0           $result->{results};
740             }
741              
742             =head1 AUTHOR
743              
744             Patrick Hochstenbach, C<< >>
745              
746             =head1 LICENSE AND COPYRIGHT
747              
748             Copyright 2015 Patrick Hochstenbach
749              
750             This program is free software; you can redistribute it and/or modify it
751             under the terms of either: the GNU General Public License as published
752             by the Free Software Foundation; or the Artistic License.
753              
754             See http://dev.perl.org/licenses/ for more information.
755              
756             =cut
757              
758             1;
759              
760