File Coverage

blib/lib/Net/Delicious.pm
Criterion Covered Total %
statement 45 414 10.8
branch 0 144 0.0
condition 0 14 0.0
subroutine 15 51 29.4
pod 19 19 100.0
total 79 642 12.3


line stmt bran cond sub pod time code
1             # $Id: Delicious.pm,v 1.71 2008/03/03 16:55:04 asc Exp $
2              
3             package Net::Delicious;
4 1     1   1074 use strict;
  1         2  
  1         57  
5              
6             $Net::Delicious::VERSION = '1.14';
7              
8             =head1 NAME
9              
10             Net::Delicious - OOP for the del.icio.us API
11              
12             =head1 SYNOPSIS
13              
14             use Net::Delicious;
15             use Log::Dispatch::Screen;
16              
17             my $del = Net::Delicious->new({user => "foo",
18             pswd => "bar"});
19              
20             foreach my $p ($del->recent_posts()) {
21             print $p->description()."\n";
22             }
23              
24             =head1 DESCRIPTION
25              
26             OOP for the del.icio.us API
27              
28             =cut
29              
30 1     1   1356 use Net::Delicious::Constants qw (:pause :response :uri);
  1         3  
  1         8  
31 1     1   980 use Net::Delicious::Config;
  1         4  
  1         36  
32              
33 1     1   1011 use HTTP::Request;
  1         21225  
  1         42  
34 1     1   945 use LWP::UserAgent;
  1         30585  
  1         42  
35 1     1   13 use URI;
  1         3  
  1         23  
36              
37 1     1   1043 use Log::Dispatch;
  1         22208  
  1         36  
38 1     1   1180 use Data::Dumper;
  1         7352  
  1         84  
39              
40 1     1   1114 use Time::HiRes;
  1         2243  
  1         6  
41              
42             # All this, just to keep track
43             # of update/all_posts stuff...
44              
45 1     1   971 use IO::AtomicFile;
  1         13851  
  1         49  
46 1     1   930 use FileHandle;
  1         1079  
  1         5  
47 1     1   1870 use File::Temp;
  1         10986  
  1         113  
48 1     1   9 use File::Spec;
  1         2  
  1         23  
49 1     1   943 use Date::Parse;
  1         7366  
  1         255  
50 1     1   1538 use English;
  1         2332  
  1         7  
51              
52             =head1 PACKAGE METHODS
53              
54             =cut
55              
56             =head2 __PACKAGE__->new(\%args || Config::Simple)
57              
58             Arguments to the Net::Delicious object may be defined in one of three ways :
59              
60             =over 4
61              
62             =item * As a single hash reference
63              
64             =item * As a reference to a I object
65              
66             =item * As a path to a file that may be read by the I.
67              
68             =back
69              
70             The first option isn't going away any time soon but should be considered as
71             deprecated. Valid hash reference arguments are :
72              
73             =over 4
74              
75             =item * B
76              
77             String. I
78              
79             Your del.icio.us username.
80              
81             =item * B
82              
83             String. I
84              
85             Your del.icio.us password.
86              
87             =item * B
88              
89             String.
90              
91             The path to a directory where the timestamp for the last
92             update to your bookmarks can be recorded. This is used by
93             the I method to prevent abusive requests.
94              
95             Default is the current user's home directory; If the home directory
96             can not be determined Net::Delicious will use a temporary directory
97             as determined by File::Temp.
98              
99             =item * B
100              
101             Boolean.
102              
103             Add a I dispatcher to log debug
104             (and higher) notices. Notices will be printed to STDERR.
105              
106             =back
107              
108             I options are expected to be grouped in a "block"
109             labeled B. Valid options are :
110              
111             =over 4
112              
113             =item * B
114              
115             String. I
116              
117             Your del.icio.us username.
118              
119             =item * B
120              
121             String. I
122              
123             Your del.icio.us password.
124              
125             =item * B
126              
127             String.
128              
129             The path to a directory where the timestamp for the last
130             update to your bookmarks can be recorded. This is used by
131             the I method to prevent abusive requests.
132              
133             Default is the current user's home directory, followed by
134             a temporary directory as determined by File::Temp.
135              
136             =item * B
137              
138             String.
139              
140             You may specify one of three XML parsers to use to handle response
141             messages from the del.icio.us servers. You many want to do this if,
142             instead of Perl-ish objects, you want to access the raw XML and parse
143             it with XPath or XSLT or some other crazy moon language.
144              
145             =over 4
146              
147             =item * B
148              
149             This uses L to parse messages. If present, all successful
150             API method calls will return, where applicable, Net::Delicious::* objects.
151              
152             =item * B
153              
154             This uses L to parse messages. If present, all successful
155             API method calls will return a I object.
156              
157             Future releases may allow responses parsed with libxml to be returned as
158             Net::Delicious::* objects.
159              
160             =item * B
161              
162             This uses L to parse messages. If present, all successful
163             API method calls will return a I object.
164              
165             Future releases may allow responses parsed with XML::XPath to be returned as
166             Net::Delicious::* objects.
167              
168             =back
169              
170             The default value is B.
171              
172             =item * B
173              
174             Boolean.
175              
176             Set to true if you are using L to parse response messages
177             from the del.icio.us servers but want to return the object's original
178             data structure rather than Net::Delicious::* objects.
179              
180             Default is false.
181              
182             =item * B
183              
184             String.
185              
186             Set the endpoint for all API calls.
187              
188             There's no particular reason you should ever need to set this unless,
189             say, this module falls horribly out of date with the API itself. Anyway,
190             now you can.
191              
192             Default is B
193              
194             =item * B
195              
196             Boolean.
197              
198             Add a I dispatcher to log debug
199             (and higher) notices. Notices will be printed to STDERR.
200              
201             =back
202              
203             Returns a Net::Delicious object or undef if there was a problem
204             creating the object.
205              
206             It is also possible to set additional config options to tweak the
207             default settings for API call parameters and API response properties.
208             Please consult the POD for L for details.
209              
210             =cut
211              
212             sub new {
213 0     0 1   my $pkg = shift;
214 0           my $args = shift;
215            
216             #
217            
218 0           my $self = {
219             '__wait' => 0,
220             '__paused' => 0,
221             };
222            
223             #
224             #
225              
226 0           my $cfg = undef;
227              
228 0 0         if (ref($args) eq "Config::Simple") {
    0          
    0          
229 0           $cfg = $args;
230             }
231              
232             elsif (ref($args->{cfg}) eq "Config::Simple") {
233 0           $cfg = $args->{cfg};
234             }
235              
236             elsif (-f $args->{cfg}) {
237 0           eval {
238 0           require Config::Simple;
239 0           $cfg = Config::Simple->new($args->{cfg});
240             };
241              
242 0 0         if ($@) {
243 0           warn "Failed to load config $args->{cfg}, $@";
244 0           return;
245             }
246             }
247              
248             else {
249 0           $cfg = Net::Delicious::Config->mk_config($args);
250              
251 0 0         if (! $cfg) {
252 0           warn "Failed to create internal config object, $!";
253 0           return;
254             }
255             }
256            
257 0           Net::Delicious::Config->merge_configs($cfg);
258 0           $self->{'__cfg'} = $cfg;
259              
260             #
261             #
262             #
263              
264 0           my $parser_cfg = $cfg->param("delicious.xml_parser");
265 0           my $parser_pkg = undef;
266              
267 0 0         if ($parser_cfg eq "libxml") {
    0          
268 0           $parser_pkg = "XML::LibXML";
269             }
270              
271             elsif ($parser_cfg eq "xpath") {
272 0           $parser_pkg = "XML::XPath";
273             }
274              
275             else {
276 0           $parser_pkg = "XML::Simple";
277             }
278            
279 0           eval "require $parser_pkg";
280              
281 0 0         if ($@) {
282 0           warn "Failed to load XML parser $parser_pkg, $@";
283 0           return;
284             }
285              
286 0           $parser_pkg->import();
287              
288             #
289             #
290             #
291              
292 0           bless $self, $pkg;
293              
294             #
295              
296 0 0         if ($self->config("delicious.debug")) {
297 0           require Log::Dispatch::Screen;
298 0           $self->logger()->add(Log::Dispatch::Screen->new(name => "debug",
299             min_level => "debug",
300             stderr => 1));
301             }
302              
303             #
304            
305 0           return $self;
306             }
307              
308             =head1 UPDATE METHODS
309              
310             =cut
311              
312             =head2 $obj->update()
313              
314             Returns return the time of the last update formatted as
315             a W3CDTF string.
316              
317             =cut
318              
319             sub update {
320 0     0 1   my $self = shift;
321              
322 0           my $res = $self->_execute_method("delicious.posts.update");
323 0 0         return ($res) ? $res->{time} : undef;
324             }
325              
326             =head1 POST METHODS
327              
328             =cut
329              
330             =head2 $obj->add_post(\%args)
331              
332             Makes a post to del.icio.us.
333              
334             Valid arguments are :
335              
336             =over 4
337              
338             =item * B
339              
340             String. I
341              
342             Url for post
343              
344             =item * B
345              
346             String.
347              
348             Description for post.
349              
350             =item * B
351              
352             String.
353              
354             Extended for post.
355              
356             =item * B
357              
358             String.
359              
360             Space-delimited list of tags.
361              
362             =item * B
363              
364             String.
365              
366             Datestamp for post, format "CCYY-MM-DDThh:mm:ssZ"
367              
368             =item * B
369              
370             Boolean. (Technically, you need to pass the string "no" but N:D will handle
371             1s and 0s.)
372              
373             Make the post private. Default is true.
374              
375             =item * B
376              
377             Boolean. (Technically, you need to pass the string "no" but N:D will handle
378             1s and 0s.)
379              
380             Don't replace post if given url has already been posted. Default is true.
381              
382             =back
383              
384             Returns true or false.
385              
386             =cut
387              
388             sub add_post {
389 0     0 1   my $self = shift;
390 0           my $args = shift;
391              
392 0           my $res = $self->_execute_method("delicious.posts.add", $args);
393              
394 0 0         if (! $self->_use_rsp_parser()) {
395 0           return $res;
396             }
397              
398 0           return $self->_isdone($res);
399             }
400              
401             =head2 $obj->delete_post(\%args)
402              
403             Delete a post from del.icio.us.
404              
405             Valid arguments are :
406              
407             =over 4
408              
409             =item * B
410              
411             String. I
412              
413             =back
414              
415             Returns true or false.
416              
417             =cut
418              
419             sub delete_post {
420 0     0 1   my $self = shift;
421 0           my $args = shift;
422              
423 0           my $res = $self->_execute_method("delicious.posts.delete", $args);
424              
425 0 0         if (! $self->_use_rsp_parser()) {
426 0           return $res;
427             }
428              
429 0           return $self->_isdone($res);
430             }
431              
432             =head2 $obj->posts_per_date(\%args)
433              
434             Get a list of dates with the number of posts at each date.
435              
436             Valid arguments are :
437              
438             =over 4
439              
440             =item * B
441              
442             String.
443              
444             Filter by this tag.
445              
446             =back
447              
448             Returns a list of I objects
449             when called in an array context.
450              
451             Returns a I object when called
452             in a scalar context.
453              
454             =cut
455              
456             sub posts_per_date {
457 0     0 1   my $self = shift;
458 0           my $args = shift;
459              
460 0           my $res = $self->_execute_method("delicious.posts.dates", $args);
461              
462 0 0         if (! $res) {
463 0           return;
464             }
465              
466 0 0         if (! $self->_use_rsp_parser()) {
467 0           return $res;
468             }
469              
470 0           my $dates = $self->_getresults($res, "date");
471 0           return $self->_buildresults("Date", $dates);
472             }
473              
474             =head2 $obj->recent_posts(\%args)
475              
476             Get a list of most recent posts, possibly filtered by tag.
477              
478             Valid arguments are :
479              
480             =over 4
481              
482             =item * B
483              
484             String.
485              
486             Filter by this tag.
487              
488             =item * B
489              
490             Int.
491              
492             Number of posts to return. Default is 20; maximum is 100
493              
494             =back
495              
496             Returns a list of I objects
497             when called in an array context.
498              
499             Returns a I object when called
500             in a scalar context.
501              
502             =cut
503              
504             sub recent_posts {
505 0     0 1   my $self = shift;
506 0           my $args = shift;
507            
508 0           my $res = $self->_execute_method("delicious.posts.recent", $args);
509            
510 0 0         if (! $res) {
511 0           return;
512             }
513            
514 0 0         if (! $self->_use_rsp_parser()) {
515 0           return $res;
516             }
517            
518 0           my $posts = $self->_getresults($res, "post");
519 0           return $self->_buildresults("Post", $posts);
520             }
521              
522             =head2 $obj->all_posts()
523              
524             Returns a list of I objects
525             when called in an array context.
526              
527             Returns a I object when called
528             in a scalar context.
529              
530             If no posts have been added between calls to this method,
531             it will return an empty list (or undef if called in a scalar
532             context.)
533              
534             =cut
535              
536             sub all_posts {
537 0     0 1   my $self = shift;
538              
539 0 0         if (! $self->_is_updated()) {
540 0           $self->logger()->info("posts have not changed since last call");
541 0           return;
542             }
543              
544 0           my $res = $self->_execute_method("delicious.posts.all");
545              
546 0 0         if (! $res) {
547 0           return;
548             }
549              
550 0 0         if (! $self->_use_rsp_parser()) {
551 0           return $res;
552             }
553            
554 0           my $posts = $self->_getresults($res, "post");
555 0           return $self->_buildresults("Post", $posts);
556             }
557              
558             =head2 $obj->posts(\%args)
559              
560             Get a list of posts on a given date, filtered by tag. If no
561             date is supplied, most recent date will be used.
562              
563             Valid arguments are :
564              
565             =over 4
566              
567             =item * B
568              
569             String.
570              
571             Filter by this tag.
572              
573             =item * B
574              
575             String.
576              
577             Filter by this date.
578              
579             =back
580              
581             Returns a list of I objects
582             when called in an array context.
583              
584             Returns a I object when called
585             in a scalar context.
586              
587             =cut
588              
589             sub posts {
590 0     0 1   my $self = shift;
591 0           my $args = shift;
592            
593             #
594              
595 0           my $res = $self->_execute_method("delicious.posts.get", $args);
596            
597 0 0         if (! $res) {
598 0           return;
599             }
600            
601 0 0         if (! $self->_use_rsp_parser()) {
602 0           return $res;
603             }
604            
605             #
606            
607 0           my $posts = $self->_getresults($res, "post");
608 0           return $self->_buildresults("Post", $posts);
609             }
610              
611             =head1 TAG METHODS
612              
613             =cut
614              
615             =head2 $obj->tags()
616              
617             Returns a list of tags.
618              
619             =cut
620              
621             sub tags {
622 0     0 1   my $self = shift;
623              
624 0           my $res = $self->_execute_method("delicious.tags.get");
625              
626 0 0         if (! $res) {
627 0           return;
628             }
629              
630 0 0         if (! $self->_use_rsp_parser()) {
631 0           return $res;
632             }
633              
634             #
635              
636 0           my $tags = $self->_getresults($res, "tag");
637 0           return $self->_buildresults("Tag", $tags);
638             }
639              
640             =head2 $obj->rename_tag(\%args)
641              
642             Renames tags across all posts.
643              
644             Valid arguments are :
645              
646             =over 4
647              
648             =item * B
649              
650             String. I
651              
652             Old tag
653              
654             =item * B
655              
656             String. I
657              
658             New tag
659              
660             =back
661              
662             Returns true or false.
663              
664             =cut
665              
666             sub rename_tag {
667 0     0 1   my $self = shift;
668 0           my $args = shift;
669              
670 0           my $res = $self->_execute_method("delicious.tags.rename", $args);
671              
672 0 0         if (! $self->_use_rsp_parser()) {
673 0           return $res;
674             }
675              
676 0           return $self->_isdone($res);
677             }
678              
679             =head2 $obj->all_posts_for_tag(\%args)
680              
681             This is a just a helper method which hides a bunch of API calls behind
682             a single method.
683              
684             Valid arguments are :
685              
686             =over 4
687              
688             =item * B
689              
690             String. I
691              
692             The tag you want to retrieve posts for.
693              
694             =back
695              
696             Returns a list of I objects
697             when called in an array context.
698              
699             Returns a I object when called
700             in a scalar context.
701              
702             =cut
703              
704             sub all_posts_for_tag {
705 0     0 1   my $self = shift;
706 0           my $args = shift;
707              
708 0 0         if (! $self->_use_rsp_parser()) {
709 0           $self->logger()->error("This method does not work with the XML parser settings you have chosen");
710 0           return;
711             }
712            
713 0   0       $args ||= {};
714            
715 0 0         if (! $args->{tag}) {
716 0           $self->logger()->error("You must specify a tag");
717 0           return;
718             }
719              
720 0           my $it = $self->posts_per_date({tag => $args->{tag}});
721              
722 0 0         if (! $it) {
723 0           return;
724             }
725              
726 0           my @posts = ();
727              
728 0           while (my $dt = $it->next()) {
729              
730 0           my @links = $self->posts({tag => $args->{tag},
731             dt => $dt->date()});
732              
733 0 0         if (wantarray) {
734 0           push @posts, @links;
735             }
736            
737             else {
738 0           map {
739 0           push @posts, $_->as_hashref();
740             } @links;
741             }
742             }
743              
744 0 0         if (wantarray) {
745 0           return @posts;
746             }
747              
748 0           return $self->_buildresults("Post", \@posts);
749             }
750              
751             =head1 BUNDLE METHODS
752              
753             =cut
754              
755             =head2 $obj->bundles()
756              
757             Returns a list of I objects
758             when called in an array context.
759              
760             Returns a I object when called
761             in a scalar context.
762              
763             =cut
764              
765             sub bundles {
766 0     0 1   my $self = shift;
767            
768 0           my $res = $self->_execute_method("delicious.tags.bundles.all");
769              
770 0 0         if (! $self->_use_rsp_parser()) {
771 0           return $res;
772             }
773            
774 0           my $bundles = $self->_getresults($res, "bundle");
775 0           $bundles = $bundles->[0];
776            
777 0 0         if (ref($bundles) ne "HASH") {
778 0           $self->logger()->error("failed to parse response");
779 0           return;
780             }
781              
782             # argh....
783              
784 0           my @data = ();
785              
786 0 0         if (exists($bundles->{name})) {
787 0           @data = $bundles;
788             }
789            
790             else {
791 0           @data = map {
792 0           {name => $_,tags => $bundles->{$_}->{'tags'} }
793             } keys %$bundles;
794             }
795            
796             #
797            
798 0           return $self->_buildresults("Bundle", \@data);
799             }
800              
801             =head2 $obj->set_bundle(\%args)
802              
803             Valid arguments are :
804              
805             =over 4
806              
807             =item * B
808              
809             String. I
810              
811             The name of the bundle to set.
812              
813             =item * B
814              
815             String. I
816              
817             A space-separated list of tags.
818              
819             =back
820              
821             Returns true or false
822              
823             =cut
824              
825             sub set_bundle {
826 0     0 1   my $self = shift;
827 0           my $args = shift;
828            
829 0           my $res = $self->_execute_method("delicious.tags.bundles.set", $args);
830              
831 0 0         if (! $self->_use_rsp_parser()) {
832 0           return $res;
833             }
834              
835 0           return $self->_isdone($res);
836             }
837              
838             =head2 $obj->delete_bundle(\%args)
839              
840             Valid arguments are :
841              
842             =over 4
843              
844             =item * B
845              
846             String. I
847              
848             The name of the bundle to set
849              
850             =back
851              
852             Returns true or false
853              
854             =cut
855              
856             sub delete_bundle {
857 0     0 1   my $self = shift;
858 0           my $args = shift;
859            
860 0           my $res = $self->_execute_method("delicious.tags.bundles.delete", $args);
861              
862 0 0         if (! $self->_use_rsp_parser()) {
863 0           return $res;
864             }
865              
866 0           return $self->_isdone($res);
867             }
868              
869             =head1 HELPER METHODS
870              
871             =cut
872              
873             =head2 $obj->logger()
874              
875             Returns a Log::Dispatch object.
876              
877             =cut
878              
879             sub logger {
880 0     0 1   my $self = shift;
881            
882 0 0         if (ref($self->{'__logger'}) ne "Log::Dispatch") {
883 0           my $log = Log::Dispatch->new();
884 0           $self->{'__logger'} = $log;
885             }
886            
887 0           return $self->{'__logger'};
888             }
889              
890             =head2 $obj->config(@args)
891              
892             This is just a short-cut for calling the current object's internal
893             Config::Simple I method. You may use to it to get and set
894             config parameters although they will not be saved to disk when the object
895             is destroyed.
896              
897             =cut
898              
899             sub config {
900 0     0 1   my $self = shift;
901 0           return $self->{'__cfg'}->param(@_);
902             }
903              
904             =head2 $obj->username()
905              
906             Returns the del.icio.us username for the current object.
907              
908             =cut
909              
910             sub username {
911 0     0 1   my $self = shift;
912 0           return $self->config("delicious.user");
913             }
914              
915             =head2 $obj->password()
916              
917             Returns the del.icio.us password for the current object.
918              
919             =cut
920              
921             sub password {
922 0     0 1   my $self = shift;
923 0           return $self->config("delicious.pswd");
924             }
925              
926             =head2 $object->user_agent()
927              
928             This returns the objects internal LWP::UserAgent in case you need to tweak
929             timeouts, proxies, etc.
930              
931             B glue.>
932              
933             =cut
934              
935             sub user_agent {
936 0     0 1   my $self = shift;
937            
938 0 0         if (ref($self->{'__ua'}) ne "LWP::UserAgent") {
939 0           my $ua = LWP::UserAgent->new();
940 0           $ua->agent(sprintf("%s, %s", __PACKAGE__, $Net::Delicious::VERSION));
941 0           $ua->env_proxy(1);
942              
943 0           $self->{'__ua'} = $ua;
944             }
945            
946 0           return $self->{'__ua'};
947             }
948              
949             #
950             # Private methods
951             #
952              
953             sub _read_update {
954 0     0     my $self = shift;
955            
956 0           my $path = $self->_path_update();
957              
958 0 0         if (! -f $path) {
959 0           return time();
960             }
961              
962 0           my $fh = FileHandle->new($path);
963            
964 0 0         if (! $fh) {
965 0           $self->logger()->error("unable to open '$path' for reading, $!");
966 0           return 0;
967             }
968            
969 0           my $time = $fh->getline();
970 0           chomp $time;
971            
972 0           $fh->close();
973 0           return $time;
974             }
975              
976             sub _write_update {
977 0     0     my $self = shift;
978 0           my $time = shift;
979            
980 0           my $path = $self->_path_update();
981 0           my $fh = IO::AtomicFile->open($path,"w");
982            
983 0 0         if (! $fh) {
984 0           $self->logger()->error("unable to open '$path' for writing, $!");
985 0           return 0;
986             }
987            
988 0           $fh->print($time);
989 0           $fh->close();
990            
991 0           return 1;
992             }
993              
994             sub _is_updated {
995 0     0     my $self = shift;
996            
997 0           my $last = $self->_read_update();
998 0           my $current = $self->update();
999            
1000 0           $self->_write_update($current);
1001            
1002 0 0         return ($last) ? (str2time($current) > str2time($last)) : 1;
1003             }
1004              
1005             sub _path_update {
1006 0     0     my $self = shift;
1007            
1008 0           my $file = sprintf(".del.icio.us.%s", $self->config("delicious.user"));
1009              
1010 0 0         if (! $self->{'__updates'}){
1011              
1012 0           my $user_cfg = $self->config("delicious.updates");
1013              
1014 0 0         if ($user_cfg) {
    0          
1015 0           $self->{'__updates'} = $user_cfg;
1016             }
1017            
1018             elsif (-d (getpwuid($EUID))[7]) {
1019 0           $self->{'__updates'} = (getpwuid($EUID))[7];
1020             }
1021            
1022            
1023             else {
1024 0           $self->{'__updates'} = File::Temp::tempdir();
1025             }
1026             }
1027              
1028 0           my $root = $self->{'__updates'};
1029 0           return File::Spec->catfile($root, $file);
1030             }
1031              
1032             sub _execute_method {
1033 0     0     my $self = shift;
1034 0           my $meth = shift;
1035 0           my $args = shift;
1036              
1037 0           my $params = $self->_validateinput($meth, $args);
1038              
1039 0 0         if (! $params) {
1040 0           return 0;
1041             }
1042              
1043 0           $meth =~ /[^\.]+\.(.*)$/;
1044 0           my $uri = $1;
1045              
1046 0           $uri =~ s/\./\//g;
1047              
1048 0           my $req = $self->_buildrequest($uri, $args, $params);
1049 0           my $res = $self->_sendrequest($req);
1050              
1051 0           return $res;
1052             }
1053              
1054             sub _validateinput {
1055 0     0     my $self = shift;
1056 0           my $block = shift;
1057 0           my $args = shift;
1058              
1059 0 0         if (! $args) {
1060 0           $args = {};
1061             }
1062              
1063 0           $block =~ s/\./_/g;
1064              
1065 0           my $rules = $self->config(-block => $block);
1066              
1067 0 0         if (! defined($rules)) {
1068 0           $self->logger()->error("Unknown error validating user input; unable to find validation rules for $block");
1069 0           return undef;
1070             }
1071              
1072 0           my @params = ();
1073              
1074 0           foreach my $param (keys %$rules) {
1075              
1076 0           my ($required, $type) = split(";", $rules->{$param});
1077              
1078 0 0 0       if (($required) && (! exists($args->{$param}))) {
1079 0           $self->logger()->error("$param is a required parameter");
1080 0           return undef;
1081             }
1082              
1083 0 0 0       if (($type) && ($type eq "no")) {
1084 0           $self->_mkno($args, $param);
1085             }
1086              
1087 0           push @params, $param;
1088             }
1089              
1090 0           return \@params;
1091             }
1092              
1093             sub _buildrequest {
1094 0     0     my $self = shift;
1095 0           my $meth = shift;
1096 0           my $args = shift;
1097 0           my $params = shift;
1098              
1099 0           my %query = map {
1100 0 0         $_ => $args->{$_}
1101             } grep {
1102 0           exists($args->{$_}) && $args->{$_}
1103             } @$params;
1104              
1105 0           my $endpoint = $self->config("delicious.endpoint");
1106 0           my $uri = URI->new_abs($meth, $endpoint);
1107              
1108 0           $uri->query_form(%query);
1109              
1110 0           my $req = HTTP::Request->new(GET => $uri);
1111 0           $self->_authorize($req);
1112              
1113             #
1114              
1115 0           $self->logger()->debug($req->as_string());
1116 0           return $req;
1117             }
1118              
1119             sub _sendrequest {
1120 0     0     my $self = shift;
1121 0           my $req = shift;
1122            
1123             # check to see if we need to take
1124             # breather (are we pounding or are
1125             # we not?)
1126            
1127 0           while (time < $self->{'__wait'}) {
1128            
1129 0           my $debug_msg = sprintf("trying not to beat up on service, pause for %.2f seconds\n",
1130             PAUSE_SECONDS_OK);
1131            
1132 0           $self->logger()->debug($debug_msg);
1133 0           sleep(PAUSE_SECONDS_OK);
1134             }
1135              
1136             #
1137             # send request
1138             #
1139              
1140 0           my $res = $self->user_agent()->request($req);
1141 0           $self->logger()->debug($res->as_string());
1142            
1143             # check for 503 status
1144            
1145 0 0         if ($res->code() eq PAUSE_ONSTATUS) {
1146            
1147             # you are in a dark and twisty corridor
1148             # where all the errors look the same -
1149             # just give up if we hit this ceiling
1150            
1151 0           $self->{'__paused'} ++;
1152            
1153 0 0         if ($self->{'__paused'} > PAUSE_MAXTRIES) {
1154            
1155 0           my $errmsg = sprintf("service returned '%d' status %d times; exiting",
1156             PAUSE_ONSTATUS,PAUSE_MAXTRIES);
1157            
1158 0           $self->logger()->error($errmsg);
1159 0           return undef;
1160             }
1161              
1162             # check to see if the del.icio.us server
1163             # requests that we hold off for a set amount
1164             # of time - otherwise wait a little longer
1165             # than the last time
1166            
1167 0           my $retry_after = $res->header("Retry-After");
1168 0           my $debug_msg = undef;
1169            
1170 0 0         if ($retry_after ) {
1171 0           $debug_msg = sprintf("service unavailable, requested to retry in %d seconds",
1172             $retry_after);
1173             }
1174            
1175             else {
1176 0           $retry_after = PAUSE_SECONDS_UNAVAILABLE * $self->{'__paused'};
1177 0           $debug_msg = sprintf("service unavailable, pause for %.2f seconds",
1178             $retry_after);
1179             }
1180            
1181 0           $self->logger()->debug($debug_msg);
1182 0           sleep($retry_after);
1183            
1184             # try, try again
1185            
1186 0           return $self->_sendrequest($req);
1187             }
1188            
1189             # (re) set internal timers
1190            
1191 0           $self->{'__wait'} = time + PAUSE_SECONDS_OK;
1192 0           $self->{'__paused'} = 0;
1193            
1194             # check for any other HTTP
1195             # errors
1196            
1197 0 0         if ($res->code() ne 200) {
1198 0           $self->logger()->error(join(":", $res->code(), $res->message()));
1199 0           return undef;
1200             }
1201            
1202 0 0         if ($res->content() =~ /^
1203 0           $self->logger()->error("erp. returned HTML - this is wrong");
1204 0           return undef;
1205             }
1206              
1207 0           return $self->_parse_xml($res);
1208             }
1209              
1210             sub _parse_xml {
1211 0     0     my $self = shift;
1212 0           my $res = shift;
1213              
1214 0           my $parser = $self->config("delicious.xml_parser");
1215 0           my $xml = undef;
1216              
1217 0           eval {
1218 0 0         if ($parser eq "libxml") {
    0          
1219 0           my $parser = XML::LibXML->new();
1220 0           $xml = $parser->parse_string($res->content());
1221             }
1222            
1223             elsif ($parser eq "xpath") {
1224 0           $xml = XML::XPath->new(xml => $res->content());
1225             }
1226            
1227             else {
1228 0           $xml = XMLin($res->content());
1229             }
1230             };
1231              
1232 0 0         if ($@) {
1233 0           $self->logger()->error("failed to parse response with $parser, $@");
1234 0           return undef;
1235             }
1236              
1237 0 0         if ($xml eq RESPONSE_ERROR) {
1238 0           $self->logger()->error($xml);
1239 0           return undef;
1240             }
1241              
1242 0           return $xml;
1243             }
1244              
1245             sub _authorize {
1246 0     0     my $self = shift;
1247 0           my $req = shift;
1248 0           $req->authorization_basic($self->username(), $self->password());
1249             }
1250              
1251             sub _ua {
1252 0     0     my $self = shift;
1253            
1254 0 0         if (ref($self->{'__ua'}) ne "LWP::UserAgent") {
1255 0           my $ua = LWP::UserAgent->new();
1256 0           $ua->agent(sprintf("%s, %s", __PACKAGE__, $Net::Delicious::VERSION));
1257            
1258 0           $self->{'__ua'} = $ua;
1259             }
1260            
1261 0           return $self->{'__ua'};
1262             }
1263              
1264             sub _getresults {
1265 0     0     my $self = shift;
1266 0           my $data = shift;
1267 0           my $key = shift;
1268            
1269 0 0         if (! exists($data->{$key})) {
    0          
1270 0           return [];
1271             }
1272            
1273             elsif (ref($data->{ $key }) eq "ARRAY") {
1274 0           return $data->{ $key };
1275             }
1276            
1277             else {
1278 0           return [ $data->{ $key } ];
1279             }
1280             }
1281              
1282             sub _buildresults {
1283 0     0     my $self = shift;
1284 0           my $type = shift;
1285 0           my $results = shift;
1286            
1287             #
1288            
1289 0           $type =~ s/:://g;
1290              
1291 0 0         if ($self->config("delicious.use_dev")) {
1292             # Debugging ... so much hate
1293 0           unshift @INC, "./lib";
1294             }
1295              
1296 0           my $fclass = join("::", __PACKAGE__, $type);
1297 0           eval "require $fclass";
1298            
1299 0 0         if ($@) {
1300 0           $self->logger()->error($@);
1301 0           return undef;
1302             }
1303              
1304 0           my $count = scalar(@$results);
1305            
1306 0           for (my $i=0; $i < $count; $i++) {
1307 0           $results->[$i] = $self->_mk_object_data($type, $results->[$i]);
1308             }
1309              
1310 0 0         if (wantarray) {
1311 0           return map {
1312 0           $fclass->new($_);
1313             } @$results;
1314             }
1315            
1316 0           require Net::Delicious::Iterator;
1317 0           return Net::Delicious::Iterator->new($fclass,
1318             $results);
1319             }
1320              
1321             sub _mk_object_data {
1322 0     0     my $self = shift;
1323 0           my $type = shift;
1324 0           my $results = shift;
1325              
1326 0           my $block = lc($type);
1327 0           my @props = split("," , $self->config("delicious_properties.$block"));
1328              
1329 0           my %object_data = map {
1330 0           $_ => $results->{$_};
1331             } @props;
1332              
1333 0           return \%object_data;
1334             }
1335              
1336             sub _use_rsp_parser {
1337 0     0     my $self = shift;
1338              
1339 0 0         if ($self->config("delicious.xml_parser") ne "simple") {
1340 0           return 0;
1341             }
1342              
1343 0 0         if ($self->config("delicious.force_xml_objects")) {
1344 0           return 0;
1345             }
1346              
1347 0           return 1;
1348             }
1349              
1350             sub _isdone {
1351 0     0     my $self = shift;
1352 0           my $res = shift;
1353              
1354 0 0 0       if (! $res) {
    0 0        
    0          
    0          
1355 0           return 0;
1356             }
1357            
1358             elsif ($res eq RESPONSE_DONE) {
1359 0           return 1;
1360             }
1361            
1362             elsif ($res eq RESPONSE_OK) {
1363 0           return 1;
1364             }
1365            
1366             elsif ((ref($res) eq "HASH") &&
1367             (exists($res->{code})) &&
1368             ($res->{code} eq RESPONSE_DONE)) {
1369            
1370 0           return 1;
1371             }
1372            
1373             else {
1374 0           $self->logger()->error("Unknown data structure returned.");
1375 0           return 0;
1376             }
1377             }
1378              
1379             # This assumes the default is true (as in not "no")
1380              
1381             sub _mkno {
1382 0     0     my $self = shift;
1383 0           my $args = shift;
1384 0           my $key = shift;
1385              
1386 0 0         if (! exists($args->{$key})) {
1387 0           return;
1388             }
1389              
1390 0 0         if ($args->{$key}) {
1391 0           delete $args->{$key};
1392 0           return;
1393             }
1394              
1395 0           $args->{$key} = "no";
1396 0           return;
1397             }
1398              
1399             =head1 ERRORS
1400              
1401             Errors are logged via the object's I method which returns
1402             a I object. If you want to get at the errors it is
1403             up to you to provide it with a dispatcher.
1404              
1405             =head1 VERSION
1406              
1407             1.13
1408              
1409             =head1 DATE
1410              
1411             $Date: 2008/03/03 16:55:04 $
1412              
1413             =head1 AUTHOR
1414              
1415             Aaron Straup Cope
1416              
1417             =head1 SEE ALSO
1418              
1419             http://del.icio.us/doc/api
1420              
1421             =head1 NOTES
1422              
1423             This package implements the API in its entirety as of I.
1424              
1425             =head1 LICENSE
1426              
1427             Copyright (c) 2004-2008, Aaron Straup Cope. All Rights Reserved.
1428              
1429             This is free software, you may use it and distribute it under the
1430             same terms as Perl itself.
1431              
1432             =cut
1433              
1434             return 1;
1435              
1436             __END__