File Coverage

blib/lib/WWW/Zotero/Write.pm
Criterion Covered Total %
statement 20 101 19.8
branch 0 30 0.0
condition 0 6 0.0
subroutine 7 21 33.3
pod 8 8 100.0
total 35 166 21.0


line stmt bran cond sub pod time code
1             package WWW::Zotero::Write;
2            
3 1     1   49667 use 5.6.0;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         48  
5 1     1   5 use warnings;
  1         4  
  1         24  
6 1     1   331 use Moo;
  1         8423  
  1         5  
7             extends 'WWW::Zotero';
8            
9 1     1   1235 use Carp;
  1         2  
  1         44  
10 1     1   392 use JSON;
  1         11316  
  1         9  
11            
12             #use Data::Dumper;
13 1     1   559 use URI::Escape;
  1         1876  
  1         1432  
14            
15             =head1 NAME
16            
17             WWW::Zotero::Write - Perl interface to the Zotero Write API
18            
19             =cut
20            
21             our $VERSION = '0.03';
22            
23             =head1 VERSION
24            
25             Version 0.03
26            
27             =cut
28            
29             =head1 DESCRIPTION
30            
31             This module use L to add, update, delete items, collections, tags or searches.
32            
33             =cut
34            
35             =head1 SYNOPSIS
36            
37             use Data::Dumper;
38             use WWW::Zotero::Write;
39             #key is the zotero key for the library
40             my $client = WWW::Zotero::Write->new(key => 'Inlfxd ... ');
41            
42             #@collections is an array of hash ref {name => $collection_name,
43             # parentCollection => $parent_collection_key}
44            
45             my ( $ok, $same, $failed ) =
46             $client->addCollections( \@collections, group => $groupid );
47            
48             unless ($ok) {
49             print Dumper ($same), "\n", Dumper($failed), "\n";
50             die "Collection not added";
51             }
52             my @keys;
53             for my $c ( sort { $a <=> $b } keys %$ok ) {
54             push @keys, $ok->{$c};
55             }
56            
57             # $keys[ $pos ] contains the key of $items[ $pos ]
58            
59             # %data is a hash of fields => values pairs.
60             # fields are key (mandatory), name, parentCollection, relations
61            
62             my ( $ok, $same, $failed ) =
63             $client->updateCollection( \%data, group => $groupid );
64            
65             # @keys is an array of collections zotero keys
66            
67             $client->deleteCollections( \@keys, group => $groupid )
68             or die("Can't delete collections");
69            
70            
71             # @modif is an array of hash ref
72             # { key => $item_key,
73             # collections => $coll_ref,
74             # version => $item_version
75             # }
76             # $coll_ref is an array ref of collections keys the item belongs to
77            
78             my ( $ok, $same, $failed ) =
79             $client->updateItems( \@modif, group => $groupid );
80             unless ($ok) {
81             print Dumper ($same), "\n", Dumper($failed), "\n";
82             die "Items collections not modidified in Zotero";
83             }
84            
85             # @itemkeys is an array of item zotero keys
86            
87             $client->deleteItems( \@itemkeys, group => $groupid ) or die("Can't delete items");
88            
89             my $template = $client->itemTemplate("book");
90             $template->{titre} = "Hello World";
91             $template->{date} = "2017";
92             # ...
93            
94             push @items, $template;
95             # @items is an array of hash ref of new data (templates completed with real values)
96            
97             my ( $ok, $same, $failed ) =
98             $client->addItems( \@items, group => $groupid );
99             unless ($ok) {
100             print Dumper ($same), "\n", Dumper($failed), "\n";
101             die "Items not added to Zotero";
102             }
103             my @keys;
104             for my $c ( sort { $a <=> $b } keys %$ok ) {
105             print $c, " ", $ok->{$c}, "\n";
106             push @keys, $ok->{$c};
107             }
108             # $keys[ $pos ] contains the key of $items[ $pos ]
109            
110             #@v is an array of tags values
111             $client->deleteTags(\@v, group=>$groupid) or die "Can't delete tags";
112            
113             =cut
114            
115             has last_modif_ver => ( is => 'rw' );
116            
117             =head2 addCollections($coll_array_ref, user => $userid | group => $groupid)
118            
119             Add an array of collection.
120            
121             Param: the array ref of hash ref with collection name and parent key
122             [{"name"=>"coll name", "parentCollection"=> "parent key"}, {}]
123            
124             Param: the group or the user id
125            
126             Returns undef if the ResponseCode is not 200 (409: Conflit, 412: Precondition failed)
127            
128             Returns an array with three hash ref (or undef if the hash are empty): changed, unchanged, failed.
129             The keys are the index of the hash received in argument. The values are the keys given by zotero
130            
131             =cut
132            
133             sub addCollections {
134 0     0 1   my ( $self, $coll, %opt ) = @_;
135 0           my ( $groupid, $userid ) = @opt{qw(group user)};
136 0           $self->_add_this( $groupid, $userid, $coll, "collections" );
137            
138             }
139            
140             =head2 updateCollection ($data, group => $groupid | user => $userid)
141            
142             Update an existing collection.
143            
144             Param: hash ref of key value pairs. The zotero key of the collection must be present in the hash.
145             Others fields are name, parentCollection, relations.
146            
147             Param: the group id (hash key: group) or the user id (hash key: user).
148            
149             Returns an array with three hash ref (or undef if the hash are empty): changed, unchanged, failed.
150            
151             =cut
152            
153             sub updateCollection {
154 0     0 1   my ( $self, $data, %opt ) = @_;
155 0           my ( $groupid, $userid ) = @opt{qw(group user)};
156 0 0         croak("Missing a collection key") unless ( $data->{key} );
157 0           my $url =
158             $self->_build_url( $groupid, $userid ) . "/collections/$data->{key}";
159 0           my $token = encode_json($data);
160 0 0         if ( !$data->{version} ) {
161 0           $self->_header_last_modif_ver( $groupid, $userid );
162             }
163 0           my $response = $self->client->PATCH( $url, $token );
164 0           return $self->_check_response( $response, "204" );
165             }
166            
167             =head2 addItems($items, group => $groupid | user => $userid)
168            
169             Add an array of items.
170            
171             Param: the array ref of hash ref with completed item templates.
172            
173             Param: the group id (hash key: group) or the user id (hash key: user).
174            
175             Returns undef if the ResponseCode is not 200 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
176            
177             Returns an array with three hash ref (or undef if the hash are empty): changed, unchanged, failed.
178            
179             The keys are the index of the hash received in argument. The values are the keys given by zotero
180            
181             =cut
182            
183             sub addItems {
184 0     0 1   my ( $self, $items, %opt ) = @_;
185 0           my ( $groupid, $userid ) = @opt{qw(group user)};
186 0           $self->_add_this( $groupid, $userid, $items, "items" );
187             }
188            
189             =head2 updateItems($data, group => $groupid | user => $userid)
190            
191             Update an array of items.
192            
193             Param: the array ref of hash ref which must include the key of the item, the version of the item and the new value.
194            
195             Param: the group id or the user id pass with the hash keys group or user.
196            
197             Returns undef if the ResponseCode is not 200 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
198            
199             Returns an array with three hash ref (or undef if the hashes are empty): changed, unchanged, failed.
200            
201             The keys are the index of the hash received in argument. The values are the keys given by zotero
202            
203             =cut
204            
205             sub updateItems {
206 0     0 1   my ( $self, $data, %opt ) = @_;
207 0 0         croak "updateItems: can't treat more then 50 elements"
208             if ( scalar @$data > 50 );
209 0           my ( $groupid, $userid ) = @opt{qw(group user)};
210 0           my $url = $self->_build_url( $groupid, $userid ) . "/items";
211 0           my $token = encode_json($data);
212 0           $self->_header_last_modif_ver( $groupid, $userid );
213 0           my $response = $self->client->POST( $url, $token );
214 0 0         $self->last_modif_ver(
215             $response->responseHeader('Last-Modified-Version') )
216             if ( $response->responseCode eq "200" );
217 0           return $self->_check_response( $response, "200" );
218             }
219            
220             =head2 deleteItems($keys, group => $groupid | user => $userid)
221            
222             Delete an array of items.
223            
224             Param: the array ref of item keys to delete.
225            
226             Param: the group or the user id, pass with the hash keys user or group.
227            
228             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
229            
230             =cut
231            
232             sub deleteItems {
233 0     0 1   my ( $self, $keys, %opt ) = @_;
234 0           my ( $groupid, $userid ) = @opt{qw(group user)};
235 0           $self->_delete_this( $groupid, $userid, $keys, "items?itemKey", "," );
236             }
237            
238             =head2 deleteCollections($keys, group => $groupid | user => $userid)
239            
240             Delete an array of collections.
241            
242             Param: the array ref of collection keys to delete.
243            
244             Param: the group or the user id, pass with the keys group or user.
245            
246             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
247            
248             =cut
249            
250             sub deleteCollections {
251 0     0 1   my ( $self, $keys, %opt ) = @_;
252 0           my ( $groupid, $userid ) = @opt{qw(group user)};
253 0           $self->_delete_this( $groupid, $userid, $keys,
254             "collections?collectionKey", "," );
255            
256             }
257            
258             =head2 deleteSearches($keys, group => $groupid | user => $userid)
259            
260             Delete an array of searches.
261            
262             Param: the array ref of search key to delete.
263            
264             Param: the group or the user id, pass with the keys group or user.
265            
266             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
267            
268             =cut
269            
270             sub deleteSearches {
271 0     0 1   my ( $self, $keys, %opt ) = @_;
272 0           my ( $groupid, $userid ) = @opt{qw(group user)};
273 0           $self->_delete_this( $groupid, $userid, $keys, "searches?searchKey",
274             "," );
275            
276             }
277            
278             =head2 deleteTags($keys, group => $groupid | user => $userid)
279            
280             Delete an array of tags.
281            
282             Param: the array ref of tags to delete.
283            
284             Param: the group or the user id, pass with the keys group or user.
285            
286             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
287            
288             =cut
289            
290             sub deleteTags {
291 0     0 1   my ( $self, $tags, %opt ) = @_;
292 0           my ( $groupid, $userid ) = @opt{qw(group user)};
293 0           my @encoded_tags = map ( uri_escape($_), @$tags );
294 0           $self->_delete_this( $groupid, $userid, \@encoded_tags, "tags?tag",
295             " || " );
296             }
297            
298             sub _delete_this {
299 0     0     my ( $self, $groupid, $userid, $data, $metadata, $sep ) = @_;
300 0 0         confess "Can't delete more then 50 elements" if ( scalar @$data > 50 );
301 0           my $url =
302             $self->_build_url( $groupid, $userid )
303             . "/$metadata="
304             . join( $sep, @$data );
305            
306 0           $self->_header_last_modif_ver( $groupid, $userid );
307 0           my $response = $self->client->DELETE($url);
308 0           return $self->_check_response( $response, "204" );
309             }
310            
311             sub _add_this {
312 0     0     my ( $self, $groupid, $userid, $data, $metadata ) = @_;
313 0 0         confess "Can't treat more then 50 elements"
314             if ( scalar @$data > 50 );
315 0           $self->_header_last_modif_ver( $groupid, $userid );
316 0           my $url = $self->_build_url( $groupid, $userid ) . "/$metadata";
317 0           my $token = encode_json($data);
318 0           my $response = $self->client->POST( $url, $token );
319 0           return $self->_check_response( $response, "200" );
320            
321             }
322            
323             sub _check_response {
324 0     0     my ( $self, $response, $success_code ) = @_;
325 0           my $code = $response->responseCode;
326 0           my $res = $response->responseContent;
327 0           $self->log->debug( "> Code: ", $code );
328 0           $self->log->debug( "> Content: ", $res );
329            
330 0 0         return unless ( $code eq $success_code );
331 0 0         if ( $success_code eq "200" ) {
332            
333 0           my $data = decode_json($res);
334            
335 0           my @results;
336 0           for my $href ( $data->{success}, $data->{unchanged}, $data->{failed} )
337             {
338 0 0         push @results, ( scalar keys %$href > 0 ? $href : undef );
339             }
340 0           return @results;
341             }
342 0           else { return 1 }
343             ; #code 204
344            
345             }
346            
347             sub _get_last_modified_version {
348 0     0     my ( $self, $groupid, $userid ) = @_;
349            
350 0           my $url = $self->_build_url( $groupid, $userid ) . "/collections/top";
351 0           my $response = $self->client->GET($url);
352 0 0         if ($response) {
353 0           my $last_modif = $response->responseHeader('Last-Modified-Version');
354 0           $self->log->debug("> Last-Modified-Version: $last_modif");
355 0           $self->last_modif_ver($last_modif);
356 0           return 1;
357             }
358 0           return 0;
359            
360             }
361            
362             sub _build_url {
363 0     0     my ( $self, $groupid, $userid ) = @_;
364 0 0 0       confess("userid or groupid missing") unless ( $groupid || $userid );
365 0 0 0       confess("userid and groupid: choose one, can't use both")
366             if ( $groupid && $userid );
367 0 0         my $id = defined $userid ? $userid : $groupid;
368 0 0         my $type = defined $userid ? 'users' : 'groups';
369            
370 0           return $self->baseurl . "/$type/$id";
371            
372             }
373            
374             sub _header_last_modif_ver {
375 0     0     my ( $self, $groupid, $userid ) = @_;
376            
377             #ensure to set the last-modified-version with querying
378             #all the top collection
379 0 0         confess("Can't get Last-Modified-Version")
380             unless ( $self->_get_last_modified_version( $groupid, $userid ) );
381 0           $self->client->addHeader( 'If-Unmodified-Since-Version',
382             $self->last_modif_ver() );
383            
384             }
385            
386             1;
387            
388             =head1 BUGS
389            
390             See support below.
391            
392             =head1 SUPPORT
393            
394             Any questions or problems can be posted to me (rappazf) on my gmail account.
395            
396             The current state of the source can be extract using Mercurial from
397             L
398            
399             =head1 AUTHOR
400            
401             FranEois Rappaz
402             CPAN ID: RAPPAZF
403            
404             =head1 COPYRIGHT
405            
406             FranEois Rappaz 2017
407             This program is free software; you can redistribute
408             it and/or modify it under the same terms as Perl itself.
409            
410             The full text of the license can be found in the
411             LICENSE file included with this module.
412            
413            
414             =head1 SEE ALSO
415            
416             L
417            
418             =cut
419