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