File Coverage

blib/lib/WWW/Zotero/Write.pm
Criterion Covered Total %
statement 20 110 18.1
branch 0 34 0.0
condition 0 6 0.0
subroutine 7 22 31.8
pod 9 9 100.0
total 36 181 19.8


line stmt bran cond sub pod time code
1             package WWW::Zotero::Write;
2            
3 1     1   71611 use 5.6.0;
  1         3  
4 1     1   7 use strict;
  1         2  
  1         20  
5 1     1   4 use warnings;
  1         2  
  1         28  
6 1     1   456 use Moo;
  1         9077  
  1         3  
7             extends 'WWW::Zotero';
8            
9 1     1   1170 use Carp;
  1         2  
  1         43  
10 1     1   505 use JSON;
  1         9674  
  1         4  
11            
12             #use Data::Dumper;
13 1     1   1046 use URI::Escape;
  1         1094  
  1         1156  
14            
15             =head1 NAME
16            
17             WWW::Zotero::Write - Perl interface to the Zotero Write API
18            
19             =cut
20            
21             our $VERSION = '0.04';
22            
23             =head1 VERSION
24            
25             Version 0.04
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 =head2 updateCollections($data, group => $groupid | user => $userid)
221            
222             Update an array of collections.
223            
224             Param: the array ref of hash ref which must include the key of the collection, and the new value.
225            
226             Param: the group id or the user id pass with the hash keys group or user.
227            
228             Returns undef if the ResponseCode is not 200 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
229            
230             Returns an array with three hash ref (or undef if the hashes are empty): changed, unchanged, failed.
231            
232             The keys are the index of the hash received in argument. The values are the keys given by zotero
233            
234             =cut
235            
236             sub updateCollections {
237 0     0 1   my ( $self, $data, %opt ) = @_;
238 0 0         croak "updateCollections: can't treat more then 50 elements"
239             if ( scalar @$data > 50 );
240 0           my ( $groupid, $userid ) = @opt{qw(group user)};
241 0           my $url = $self->_build_url( $groupid, $userid ) . "/collections";
242 0           my $token = encode_json($data);
243 0           $self->_header_last_modif_ver( $groupid, $userid );
244 0           my $response = $self->client->POST( $url, $token );
245 0 0         $self->last_modif_ver(
246             $response->responseHeader('Last-Modified-Version') )
247             if ( $response->responseCode eq "200" );
248 0           return $self->_check_response( $response, "200" );
249             }
250            
251             =head2 deleteItems($keys, group => $groupid | user => $userid)
252            
253             Delete an array of items.
254            
255             Param: the array ref of item keys to delete.
256            
257             Param: the group or the user id, pass with the hash keys user or group.
258            
259             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
260            
261             =cut
262            
263             sub deleteItems {
264 0     0 1   my ( $self, $keys, %opt ) = @_;
265 0           my ( $groupid, $userid ) = @opt{qw(group user)};
266 0           $self->_delete_this( $groupid, $userid, $keys, "items?itemKey", "," );
267             }
268            
269             =head2 deleteCollections($keys, group => $groupid | user => $userid)
270            
271             Delete an array of collections.
272            
273             Param: the array ref of collection keys to delete.
274            
275             Param: the group or the user id, pass with the keys group or user.
276            
277             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
278            
279             =cut
280            
281             sub deleteCollections {
282 0     0 1   my ( $self, $keys, %opt ) = @_;
283 0           my ( $groupid, $userid ) = @opt{qw(group user)};
284 0           $self->_delete_this( $groupid, $userid, $keys,
285             "collections?collectionKey", "," );
286            
287             }
288            
289             =head2 deleteSearches($keys, group => $groupid | user => $userid)
290            
291             Delete an array of searches.
292            
293             Param: the array ref of search key to delete.
294            
295             Param: the group or the user id, pass with the keys group or user.
296            
297             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
298            
299             =cut
300            
301             sub deleteSearches {
302 0     0 1   my ( $self, $keys, %opt ) = @_;
303 0           my ( $groupid, $userid ) = @opt{qw(group user)};
304 0           $self->_delete_this( $groupid, $userid, $keys, "searches?searchKey",
305             "," );
306            
307             }
308            
309             =head2 deleteTags($keys, group => $groupid | user => $userid)
310            
311             Delete an array of tags.
312            
313             Param: the array ref of tags to delete.
314            
315             Param: the group or the user id, pass with the keys group or user.
316            
317             Returns undef if the ResponseCode is not 204 (see https://www.zotero.org/support/dev/web_api/v3/write_requests).
318            
319             =cut
320            
321             sub deleteTags {
322 0     0 1   my ( $self, $tags, %opt ) = @_;
323 0           my ( $groupid, $userid ) = @opt{qw(group user)};
324 0           my @encoded_tags = map ( uri_escape($_), @$tags );
325 0           $self->_delete_this( $groupid, $userid, \@encoded_tags, "tags?tag",
326             " || " );
327             }
328            
329             sub _delete_this {
330 0     0     my ( $self, $groupid, $userid, $data, $metadata, $sep ) = @_;
331 0 0         confess "Can't delete more then 50 elements" if ( scalar @$data > 50 );
332 0           my $url =
333             $self->_build_url( $groupid, $userid )
334             . "/$metadata="
335             . join( $sep, @$data );
336            
337 0           $self->_header_last_modif_ver( $groupid, $userid );
338 0           my $response = $self->client->DELETE($url);
339 0           return $self->_check_response( $response, "204" );
340             }
341            
342             sub _add_this {
343 0     0     my ( $self, $groupid, $userid, $data, $metadata ) = @_;
344 0 0         confess "Can't treat more then 50 elements"
345             if ( scalar @$data > 50 );
346 0           $self->_header_last_modif_ver( $groupid, $userid );
347 0           my $url = $self->_build_url( $groupid, $userid ) . "/$metadata";
348 0           my $token = encode_json($data);
349 0           my $response = $self->client->POST( $url, $token );
350 0           return $self->_check_response( $response, "200" );
351            
352             }
353            
354             sub _check_response {
355 0     0     my ( $self, $response, $success_code ) = @_;
356 0           my $code = $response->responseCode;
357 0           my $res = $response->responseContent;
358 0           $self->log->debug( "> Code: ", $code );
359 0           $self->log->debug( "> Content: ", $res );
360            
361 0 0         return unless ( $code eq $success_code );
362 0 0         if ( $success_code eq "200" ) {
363            
364 0           my $data = decode_json($res);
365            
366 0           my @results;
367 0           for my $href ( $data->{success}, $data->{unchanged}, $data->{failed} )
368             {
369 0 0         push @results, ( scalar keys %$href > 0 ? $href : undef );
370             }
371 0           return @results;
372             }
373 0           else { return 1 }
374             ; #code 204
375            
376             }
377            
378             sub _get_last_modified_version {
379 0     0     my ( $self, $groupid, $userid ) = @_;
380            
381 0           my $url = $self->_build_url( $groupid, $userid ) . "/collections/top";
382 0           my $response = $self->client->GET($url);
383 0 0         if ($response) {
384 0           my $last_modif = $response->responseHeader('Last-Modified-Version');
385 0           $self->log->debug("> Last-Modified-Version: $last_modif");
386 0           $self->last_modif_ver($last_modif);
387 0           return 1;
388             }
389 0           return 0;
390            
391             }
392            
393             sub _build_url {
394 0     0     my ( $self, $groupid, $userid ) = @_;
395 0 0 0       confess("userid or groupid missing") unless ( $groupid || $userid );
396 0 0 0       confess("userid and groupid: choose one, can't use both")
397             if ( $groupid && $userid );
398 0 0         my $id = defined $userid ? $userid : $groupid;
399 0 0         my $type = defined $userid ? 'users' : 'groups';
400            
401 0           return $self->baseurl . "/$type/$id";
402            
403             }
404            
405             sub _header_last_modif_ver {
406 0     0     my ( $self, $groupid, $userid ) = @_;
407            
408             #ensure to set the last-modified-version with querying
409             #all the top collection
410 0 0         confess("Can't get Last-Modified-Version")
411             unless ( $self->_get_last_modified_version( $groupid, $userid ) );
412 0           $self->client->addHeader( 'If-Unmodified-Since-Version',
413             $self->last_modif_ver() );
414            
415             }
416            
417             1;
418            
419             =head1 BUGS
420            
421             See support below.
422            
423             =head1 SUPPORT
424            
425             Any questions or problems can be posted to me (rappazf) on my gmail account.
426            
427             The current state of the source can be extract using Mercurial from
428             L
429            
430             =head1 AUTHOR
431            
432             FranEois Rappaz
433             CPAN ID: RAPPAZF
434            
435             =head1 COPYRIGHT
436            
437             FranEois Rappaz 2017
438             This program is free software; you can redistribute
439             it and/or modify it under the same terms as Perl itself.
440            
441             The full text of the license can be found in the
442             LICENSE file included with this module.
443            
444            
445             =head1 SEE ALSO
446            
447             L
448            
449             =cut
450