File Coverage

blib/lib/Atompub/Client.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Atompub::Client;
2              
3 2     2   54176 use warnings;
  2         4  
  2         94  
4 2     2   10 use strict;
  2         4  
  2         64  
5              
6 2     2   1154 use Atompub;
  0            
  0            
7             use Atompub::DateTime qw(datetime);
8             use Atompub::MediaType qw(media_type);
9             use Atompub::Util qw(is_acceptable_media_type is_allowed_category);
10             use Digest::SHA qw(sha1);
11             use Encode qw(encode_utf8);
12             use File::Slurp;
13             use HTTP::Status;
14             use MIME::Base64 qw(encode_base64);
15             use NEXT;
16             use URI::Escape;
17             use XML::Atom::Entry;
18             use XML::Atom::Service;
19              
20             use base qw(XML::Atom::Client Class::Accessor::Fast);
21              
22             my @ATTRS = qw(request response resource);
23             __PACKAGE__->mk_accessors(@ATTRS, qw(ua info cache));
24              
25             *req = \&request;
26             *res = \&response;
27             *rc = \&resource;
28              
29             sub init {
30             my $client = shift;
31             $client->NEXT::init(@_);
32             $client->ua->agent('Atompub::Client/'.Atompub->VERSION);
33             $client->info(Atompub::Client::Info->instance);
34             $client->cache(Atompub::Client::Cache->instance);
35             $client;
36             }
37              
38             sub proxy {
39             my($client, $proxy) = @_;
40             $client->ua->proxy(['http', 'https'], $proxy);
41             }
42              
43             sub getService {
44             my($client, $uri) = @_;
45             return $client->error('No URI') unless $uri;
46             $client->_get_service({ uri => $uri }) or return;
47             $client->rc;
48             }
49              
50             sub getCategories {
51             my($client, $uri) = @_;
52             return $client->error('No URI') unless $uri;
53             $client->_get_categories({ uri => $uri }) or return;
54             $client->rc;
55             }
56              
57             sub getFeed {
58             my($client, $uri) = @_;
59             return $client->error('No URI') unless $uri;
60             $client->_get_feed({ uri => $uri }) or return;
61             $client->rc;
62             }
63              
64             sub createEntry {
65             my($client, $uri, $entry, $slug) = @_;
66             return $client->error('No URI') unless $uri;
67             return $client->error('No Entry') unless $entry;
68             unless (UNIVERSAL::isa($entry, 'XML::Atom::Entry')) {
69             $entry = XML::Atom::Entry->new($entry)
70             or return $client->error(XML::Atom::Entry->errstr);
71             }
72             my $headers = HTTP::Headers->new;
73             $headers->content_type(media_type('entry'));
74             $headers->slug(_escape(uri_unescape $slug)) if defined $slug;
75             $client->_create_resource({
76             uri => $uri,
77             rc => $entry,
78             headers => $headers,
79             }) or return;
80             $client->res->location;
81             }
82              
83             sub createMedia {
84             my($client, $uri, $stream, $content_type, $slug) = @_;
85             return $client->error('No URI') unless $uri;
86             return $client->error('No stream') unless $stream;
87             return $client->error('No Content-Type') unless $content_type;
88             my $media = ref $stream ? $$stream : read_file($stream, binmode => ':raw')
89             or return $client->error('No media');
90             my $headers = HTTP::Headers->new;
91             $headers->content_type($content_type);
92             $headers->slug(_escape( uri_unescape $slug)) if defined $slug;
93             $client->_create_resource({
94             uri => $uri,
95             rc => \$media,
96             headers => $headers,
97             }) or return;
98             $client->res->location;
99             }
100              
101             sub getEntry {
102             my($client, $uri) = @_;
103             return $client->error('No URI') unless $uri;
104             $client->_get_resource({ uri => $uri }) or return;
105             return $client->error('Response is not Atom Entry')
106             unless UNIVERSAL::isa($client->rc, 'XML::Atom::Entry');
107             $client->rc;
108             }
109              
110             sub getMedia {
111             my($client, $uri) = @_;
112             return $client->error('No URI') unless $uri;
113             $client->_get_resource({ uri => $uri }) or return;
114             return $client->error('Response is not Media Resource')
115             if UNIVERSAL::isa($client->rc, 'XML::Atom::Entry');
116             wantarray ? ($client->rc, $client->res->content_type) : $client->rc;
117             }
118              
119             sub updateEntry {
120             my($client, $uri, $entry) = @_;
121             return $client->error('No URI') unless $uri;
122             return $client->error('No Entry') unless $entry;
123             unless (UNIVERSAL::isa( $entry, 'XML::Atom::Entry')) {
124             $entry = XML::Atom::Entry->new($entry)
125             or return $client->error(XML::Atom::Entry->errstr);
126             }
127             my $headers = HTTP::Headers->new;
128             $headers->content_type(media_type('entry'));
129             $client->_update_resource({
130             uri => $uri,
131             rc => $entry,
132             headers => $headers,
133             });
134             }
135              
136             sub updateMedia {
137             my($client, $uri, $stream, $content_type) = @_;
138             return $client->error('No URI') unless $uri;
139             return $client->error('No stream') unless $stream;
140             return $client->error('No Content-Type') unless $content_type;
141             my $media = ref $stream ? $$stream : read_file($stream, binmode => ':raw')
142             or return $client->error('No media resource');
143             my $headers = HTTP::Headers->new;
144             $headers->content_type($content_type);
145             $client->_update_resource({
146             uri => $uri,
147             rc => \$media,
148             headers => $headers,
149             });
150             }
151              
152             sub deleteEntry {
153             my($client, $uri) = @_;
154             return $client->error('No URI') unless $uri;
155             $client->_delete_resource({ uri => $uri });
156             }
157              
158             *deleteMedia = \&deleteEntry;
159              
160             sub _get_service {
161             my($client, $args) = @_;
162             my $uri = $args->{uri};
163             $client->_clear;
164             return $client->error('No URI') unless $uri;
165             $client->req(HTTP::Request->new(GET => $uri));
166             $client->res($client->make_request($client->req));
167             return $client->error(join "\n", $client->res->status_line, $client->res->content)
168             unless is_success $client->res->code;
169             warn 'Bad Content-Type: '.$client->res->content_type
170             unless media_type($client->res->content_type)->is_a('service');
171             $client->rc(XML::Atom::Service->new(\$client->res->content))
172             or return $client->error(XML::Atom::Service->errstr);
173             for my $work ($client->rc->workspaces) {
174             $client->info->put($_->href, $_) for $work->collections;
175             }
176             $client;
177             }
178              
179             sub _get_categories {
180             my($client, $args) = @_;
181             my $uri = $args->{uri};
182             $client->_clear;
183             return $client->error('No URI') unless $uri;
184             $client->req(HTTP::Request->new(GET => $uri));
185             $client->res($client->make_request($client->req));
186             return $client->error(join "\n", $client->res->status_line, $client->res->content)
187             unless is_success $client->res->code;
188             warn 'Bad Content-Type: '.$client->res->content_type
189             unless media_type($client->res->content_type)->is_a('categories');
190             $client->rc(XML::Atom::Categories->new(\$client->res->content))
191             or return $client->error(XML::Atom::Categories->errstr);
192             $client;
193             }
194              
195             sub _get_feed {
196             my($client, $args) = @_;
197             my $uri = $args->{uri};
198             $client->_clear;
199             return $client->error('No URI') unless $uri;
200             $client->req(HTTP::Request->new(GET => $uri));
201             $client->res($client->make_request($client->req));
202             return $client->error(join "\n", $client->res->status_line, $client->res->content)
203             unless is_success $client->res->code;
204             warn 'Bad Content-Type: '.$client->res->content_type
205             unless media_type($client->res->content_type)->is_a('feed');
206             $client->rc(XML::Atom::Feed->new(\$client->res->content))
207             or return $client->error(XML::Atom::Feed->errstr);
208             $client;
209             }
210              
211             sub _create_resource {
212             my($client, $args) = @_;
213              
214             my $uri = $args->{uri};
215             my $rc = $args->{resource} || $args->{rc};
216             my $headers = $args->{headers};
217              
218             $client->_clear;
219              
220             return $client->error('No URI') unless $uri;
221             return $client->error('No resource') unless $rc;
222             return $client->error('No headers') unless $headers;
223              
224             my $content_type = $headers->content_type;
225             my $info = $client->info->get($uri);
226              
227             return $client->error("Unsupported media type: $content_type")
228             unless is_acceptable_media_type($info, $content_type);
229              
230             my $content;
231             if (UNIVERSAL::isa($rc, 'XML::Atom::Entry')) {
232             my $entry = $rc;
233             return $client->error('Forbidden category')
234             unless is_allowed_category($info, $entry->category);
235             $content = $entry->as_xml;
236             XML::Atom::Client::_utf8_off($content);
237             $headers->content_type(media_type('entry'));
238             $headers->content_length(length $content);
239             }
240             elsif (UNIVERSAL::isa($rc, 'SCALAR')) {
241             $content = $$rc;
242             }
243              
244             $client->req(HTTP::Request->new(POST => $uri, $headers, $content));
245             $client->res($client->make_request($client->req));
246              
247             return $client->error(join "\n", $client->res->status_line, $client->res->content)
248             unless is_success $client->res->code;
249              
250             warn 'Bad status code: '.$client->res->code
251             unless $client->res->code == RC_CREATED;
252              
253             return $client->error('No Locaiton') unless $client->res->location;
254              
255             # warn 'No Content-Locaiton' unless $client->res->content_location;
256              
257             return $client unless $client->res->content;
258              
259             warn 'Bad Content-Type: '.$client->res->content_type
260             unless media_type($client->res->content_type)->is_a('entry');
261              
262             $client->rc(XML::Atom::Entry->new(\$client->res->content))
263             or return $client->error(XML::Atom::Entry->errstr);
264              
265             my $last_modified = $client->res->last_modified;
266             my $etag = $client->res->etag;
267              
268             $client->cache->put($client->res->location, {
269             rc => $client->rc,
270             last_modified => $last_modified,
271             etag => $etag,
272             });
273              
274             $client;
275             }
276              
277             sub _get_resource {
278             my($client, $args) = @_;
279              
280             my $uri = $args->{uri};
281              
282             $client->_clear;
283              
284             return $client->error('No URI') unless $uri;
285              
286             my $headers = HTTP::Headers->new;
287              
288             my $cache = $client->cache->get($uri);
289             if ($cache) {
290             $headers->if_modified_since(datetime($cache->last_modified)->epoch)
291             if $cache->last_modified;
292             $headers->if_none_match($cache->etag) if defined $cache->etag;
293             }
294              
295             $client->req(HTTP::Request->new(GET => $uri, $headers));
296              
297             $client->res($client->make_request($client->req));
298              
299             if (is_success $client->res->code) {
300             if (media_type($client->res->content_type)->is_a('entry')) {
301             $client->rc(XML::Atom::Entry->new(\$client->res->content))
302             or return $client->error(XML::Atom::Entry->errstr);
303             }
304             else {
305             $client->rc($client->res->content);
306             }
307              
308             my $last_modified = $client->res->last_modified;
309             my $etag = $client->res->etag;
310              
311             $client->cache->put($uri, {
312             rc => $client->rc,
313             last_modified => $last_modified,
314             etag => $etag,
315             });
316             }
317             elsif ($client->res->code == RC_NOT_MODIFIED) {
318             $client->rc($cache->rc);
319             }
320             else {
321             return $client->error(join "\n", $client->res->status_line, $client->res->content);
322             }
323              
324             $client;
325             }
326              
327             sub _update_resource {
328             my($client, $args) = @_;
329              
330             my $uri = $args->{uri};
331             my $rc = $args->{resource} || $args->{rc};
332             my $headers = $args->{headers};
333              
334             $client->_clear;
335              
336             return $client->error('No URI') unless $uri;
337             return $client->error('No resource') unless $rc;
338             return $client->error('No headers') unless $headers;
339              
340             my $content;
341             if (UNIVERSAL::isa($rc, 'XML::Atom::Entry')) {
342             my $entry = $rc;
343              
344             $content = $entry->as_xml;
345             XML::Atom::Client::_utf8_off($content);
346             $headers->content_type(media_type('entry'));
347             $headers->content_length(length $content);
348             }
349             elsif (UNIVERSAL::isa($rc, 'SCALAR')) {
350             $content = $$rc;
351             }
352              
353             if (my $cache = $client->cache->get($uri)) {
354             $headers->if_unmodified_since(datetime($cache->last_modified)->epoch)
355             if $cache->last_modified;
356             $headers->if_match($cache->etag) if defined $cache->etag;
357             }
358              
359             $client->req(HTTP::Request->new(PUT => $uri, $headers, $content));
360             $client->res($client->make_request($client->req));
361              
362             return $client->error(join "\n", $client->res->status_line, $client->res->content)
363             unless is_success $client->res->code;
364              
365             return $client unless $client->res->content;
366              
367             if (media_type($client->res->content_type)->is_a('entry')) {
368             $client->rc(XML::Atom::Entry->new(\$client->res->content))
369             or return $client->error(XML::Atom::Entry->errstr);
370             }
371             else {
372             $client->rc($client->res->content);
373             }
374              
375             my $last_modified = $client->res->last_modified;
376             my $etag = $client->res->etag;
377              
378             $client->cache->put($uri, {
379             rc => $client->rc,
380             last_modified => $last_modified,
381             etag => $etag,
382             });
383              
384             $client;
385             }
386              
387             sub _delete_resource {
388             my($client, $args) = @_;
389              
390             my $uri = $args->{uri};
391              
392             $client->_clear;
393              
394             return $client->error('No URI') unless $uri;
395              
396             my $headers = HTTP::Headers->new;
397              
398             # If-Match nor If-Unmodified-Since header is not required on DELETE
399             # if (my $cache = $client->cache->get($uri)) {
400             # $headers->if_unmodified_since(datetime($cache->last_modified)->epoch)
401             # if $cache->last_modified;
402             # $headers->if_match($cache->etag) if defined $cache->etag;
403             # }
404              
405             $client->req(HTTP::Request->new(DELETE => $uri, $headers));
406             $client->res($client->make_request($client->req));
407              
408             return $client->error(join "\n", $client->res->status_line, $client->res->content)
409             unless is_success $client->res->code;
410              
411             $client;
412             }
413              
414             sub _clear {
415             my($client) = @_;
416             $client->error('');
417             $client->{$_} = undef for @ATTRS;
418             }
419              
420             sub munge_request {
421             my($client, $req) = @_;
422              
423             $req->accept(join(',',
424             media_type('entry')->without_parameters,
425             media_type('service'), media_type('categories'),
426             '*/*',
427             ));
428              
429             return unless $client->username;
430              
431             my $nonce = sha1(sha1(time.{}.rand().$$));
432             my $now = datetime->w3cz;
433              
434             my $wsse = sprintf(
435             qq{UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"},
436             ($client->username || ''),
437             encode_base64(sha1($nonce.$now.($client->password || '')), ''),
438             encode_base64($nonce, ''),
439             $now,
440             );
441              
442             $req->header('X-WSSE' => $wsse);
443             $req->authorization('WSSE profile="UsernameToken"');
444             }
445              
446             # see 9.7.1 in RFC 5023
447             sub _escape {
448             my ($slug) = @_;
449             return uri_escape(encode_utf8($slug), "\x00-\x19\x25-\x25\x7e-\xff");
450             }
451              
452             package Atompub::Client::Info;
453              
454             my $Info;
455              
456             sub instance {
457             my($class) = @_;
458             $Info ||= bless { info => {} }, $class;
459             $Info;
460             }
461              
462             sub put {
463             my($self, $uri, @args) = @_;
464             return unless $uri;
465             if (@args) {
466             $self->{info}{$uri} = $self->_clone_collection(@args);
467             }
468             else {
469             delete $self->{info}{$uri};
470             }
471             }
472              
473             sub get {
474             my($self, $uri) = @_;
475             return unless $uri;
476             $self->{info}{$uri};
477             }
478              
479             sub _get_categories {
480             my($self, $client, $href) = @_;
481             return unless $client;
482             $client->getCategories($href);
483             }
484              
485             sub _clone_collection {
486             my($self, $coll_arg, $client) = @_;
487             return unless UNIVERSAL::isa($coll_arg, 'XML::Atom::Collection');
488             my $coll = XML::Atom::Collection->new;
489             $coll->title($coll_arg->title);
490             $coll->href($coll_arg->href);
491             $coll->accept($coll_arg->accepts) if $coll_arg->accept;
492             my @cats = grep { defined $_ }
493             map { $_->href ? $self->_get_categories($client, $_->href)
494             : $self->_clone_categories($_) }
495             $coll_arg->categories;
496             $coll->categories(@cats);
497             $coll;
498             }
499              
500             sub _clone_categories {
501             my($self, $cats_arg) = @_;
502             my $cats = XML::Atom::Categories->new;
503             $cats->fixed($cats_arg->fixed) if $cats_arg->fixed;
504             $cats->scheme($cats_arg->scheme) if $cats_arg->scheme;
505             my @cat = map { my $cat = XML::Atom::Category->new;
506             $cat->term($_->term);
507             $cat->scheme($_->scheme) if $_->scheme;
508             $cat->label($_->label) if $_->label;
509             $cat }
510             $cats_arg->category;
511             $cats->category(@cat);
512             $cats;
513             }
514              
515             package Atompub::Client::Cache;
516              
517             my $Cache;
518              
519             sub instance {
520             my($class) = @_;
521             $Cache ||= bless { cache => {} }, $class;
522             $Cache;
523             }
524              
525             sub put {
526             my($self, $uri, @args) = @_;
527             return unless $uri;
528             if (@args) {
529             $self->{cache}{$uri} = Atompub::Client::Cache::Resource->new(@args);
530             }
531             else {
532             delete $self->{cache}{$uri};
533             }
534             }
535              
536             sub get {
537             my($self, $uri) = @_;
538             return unless $uri;
539             $self->{cache}{$uri};
540             }
541              
542             package Atompub::Client::Cache::Resource;
543              
544             use strict;
545             use warnings;
546             use base qw(Class::Accessor::Fast);
547              
548             __PACKAGE__->mk_accessors(qw(resource last_modified etag));
549              
550             *rc = \&resource;
551              
552             sub new {
553             my($class, $args) = @_;
554             my $rc = $args->{resource} || $args->{rc} or return;
555             bless {
556             resource => $rc,
557             last_modified => $args->{last_modified},
558             etag => $args->{etag},
559             }, $class;
560             }
561              
562             1;
563             __END__
564              
565             =head1 NAME
566              
567             Atompub::Client - A client for the Atom Publishing Protocol
568              
569              
570             =head1 SYNOPSIS
571              
572             use Atompub::Client;
573              
574             my $client = Atompub::Client->new;
575             $client->username('Melody');
576             $client->password('Nelson');
577             #$client->proxy( $proxy_uri );
578              
579             # Get a Service Document
580             my $service = $client->getService($service_uri);
581              
582             my @workspaces = $service->workspaces;
583             my @collections = $workspaces[0]->collections;
584              
585             # CRUD an Entry Resource; assuming that the 0-th collection supports
586             # Entry Resources
587             my $collection_uri = $collections[0]->href;
588              
589             my $name = 'New Post';
590              
591             my $entry = XML::Atom::Entry->new;
592             $entry->title($name);
593             $entry->content('Content of my post.');
594              
595             my $edit_uri = $client->createEntry($collection_uri, $entry, $name);
596              
597             my $feed = $client->getFeed($collection_uri);
598             my @entries = $feed->entries;
599              
600             $entry = $client->getEntry($edit_uri);
601              
602             $client->updateEntry($edit_uri, $entry);
603              
604             $client->deleteEntry($edit_uri);
605              
606             # CRUD a Media Resource; assuming that the 1-st collection supports
607             # Media Resources
608             my $collection_uri = $collections[1]->href;
609              
610             my $name = 'My Photo';
611              
612             my $edit_uri = $client->createMedia($collection_uri, 'sample1.png',
613             'image/png', $name);
614              
615             # Get a href attribute of an "edit-media" link
616             my $edit_media_uri = $client->resource->edit_media_link;
617              
618             my $binary = $client->getMedia($edit_media_uri);
619              
620             $client->updateMedia($edit_media_uri, 'sample2.png', 'image/png');
621              
622             $client->deleteEntry($edit_media_uri);
623              
624             # Access to the requested HTTP::Request object
625             my $request = $client->request;
626              
627             # Access to the received HTTP::Response object
628             my $response = $client->response;
629              
630             # Access to the received resource (XML::Atom object or binary data)
631             my $resource = $client->resource;
632              
633              
634             =head1 DESCRIPTION
635              
636             L<Atompub::Client> implements a client for the Atom Publishing Protocol
637             described at L<http://www.ietf.org/rfc/rfc5023.txt>.
638              
639             The client supports the following features:
640              
641             =over 4
642              
643             =item * Authentication
644              
645             L<Atompub::Client> supports the Basic and WSSE Authentication described in
646             L<http://www.intertwingly.net/wiki/pie/DifferentlyAbledClients>.
647              
648             =item * Service Document
649              
650             L<Atompub::Client> understands Service Documents,
651             in which information of collections are described,
652             such as URIs, acceptable media types, and allowable categories.
653              
654             =item * Media Resource support
655              
656             Media Resources (binary data) as well as Entry Resources are supported.
657             You can create and edit Media Resources such as image and video
658             by using L<Atompub::Client>.
659              
660             =item * Media type check
661              
662             L<Atompub::Client> checks media types of resources
663             before creating and editing them to the collection.
664             Acceptable media types are shown in I<app:accept> elements in the Service Document.
665              
666             =item * Category check
667              
668             L<Atompub::Client> checks categories in Entry Resources
669             before creating and editing them to the collection.
670             Allowable categories are shown in I<app:categories> elements in the Service Document.
671              
672             =item * Cache controll and versioning
673              
674             On-memory cache and versioning, which are controlled by I<ETag> and I<Last-Modified> header,
675             are implemented in L<Atompub::Client>.
676              
677             =item * Naming resources by I<Slug> header
678              
679             The client can specify I<Slug> header when creating a resource,
680             which may be used as part of the resource URI.
681              
682             =back
683              
684              
685             =head1 METHODS
686              
687             =head2 Atompub::Client->new([ %options ])
688              
689             Creates a new Atompub client object.
690             The options are same as L<LWP::UserAgent>.
691              
692              
693             =head2 $client->getService($service_uri)
694              
695             Retrieves a Service Document at URI $service_uri.
696              
697             Returns an L<XML::Atom::Service> object on success, false otherwise.
698              
699              
700             =head2 $client->getCategories($category_uri)
701              
702             Retrieves a Category Document at URI $category_uri.
703              
704             Returns an L<XML::Atom::Categories> object on success, false otherwise.
705              
706              
707             =head2 $client->getFeed($collection_uri)
708              
709             Retrieves a Feed Document from the collection at URI $collection_uri.
710              
711             Returns an L<XML::Atom::Feed> object, false otherwise.
712              
713              
714             =head2 $client->createEntry($collection_uri, $entry, [ $slug ])
715              
716             Creates a new entry in the collection at URI $collection_uri.
717              
718             $entry must be an L<XML::Atom::Entry> object.
719              
720             If $slug is provided, it is set in I<Slug> header and may be used
721             as part of the resource URI.
722              
723             Returns a I<Location> header, which contains a URI of the newly created resource,
724             or false on error.
725              
726              
727             =head2 $client->createMedia($collection_uri, $media, $media_type, [ $slug ])
728              
729             Creates a new Media Resource and a Media Link Entry in the collection
730             at URI $collection_uri.
731              
732             If $media is a reference to a scalar, it is treated as the binary.
733             If a scalar, treated as a file containing the Media Resource.
734              
735             $media_type is the media type of the Media Resource, such as 'image/png'.
736              
737             $slug is set in the I<Slug> header, and may be used as part of
738             the resource URI.
739              
740             Returns a I<Location> header, which contains a URI of the newly created resource,
741             or false on error.
742              
743              
744             =head2 $client->getEntry($edit_uri)
745              
746             Retrieves an Entry Document with the given URI $edit_uri.
747              
748             Returns an L<XML::Atom::Entry> object on success, false otherwise.
749             If the server returns 304 (Not Modified), returns a cache of the Media Resource.
750              
751              
752             =head2 $client->getMedia($edit_uri)
753              
754             Retrieves a Media Resource with the given URI $edit_uri.
755              
756             Returns binary data of the Media Resource on success, false otherwise.
757             If the server returns 304 (Not Modified), returns a cache of the Media Resource.
758              
759              
760             =head2 $client->updateEntry($edit_uri, $entry)
761              
762             Updates the Entry Document at URI $edit_uri with the new Entry Document $entry,
763             which must be an L<XML::Atom::Entry> object.
764              
765             Returns true on success, false otherwise.
766              
767              
768             =head2 $client->updateMedia($edit_uri, $media, $media_type)
769              
770             Updates the Media Resource at URI $edit_uri with the $media.
771              
772             If $media is a reference to a scalar, it is treated as the binary.
773             If a scalar, treated as a file containing the Media Resource.
774              
775             $media_type is the media type of the Media Resource, such as 'image/png'.
776              
777             Returns true on success, false otherwise.
778              
779              
780             =head2 $client->deleteEntry($edit_uri)
781              
782             Deletes the Entry Document at URI $edit_uri.
783              
784             Returns true on success, false otherwise.
785              
786              
787             =head2 $client->deleteMedia($edit_uri)
788              
789             Deletes the Media Resource at URI $edit_uri and related Media Link Entry.
790              
791             Returns true on success, false otherwise.
792              
793              
794             =head1 Accessors
795              
796             =head2 $client->username([ $username ])
797              
798             If called with an argument, sets the username for login to $username.
799              
800             Returns the current username that will be used when logging in to the
801             Atompub server.
802              
803              
804             =head2 $client->password([ $password ])
805              
806             If called with an argument, sets the password for login to $password.
807              
808             Returns the current password that will be used when logging in to the
809             Atompub server.
810              
811              
812             =head2 $client->proxy([ $proxy_uri ])
813              
814             If called with an argument, sets URI of proxy server like 'http://proxy.example.com:8080'.
815              
816             Returns the current URI of the proxy server.
817              
818              
819             =head2 $client->resource
820              
821             =head2 $client->rc
822              
823             An accessor for Entry or Media Resource, which was retrieved in the previous action.
824              
825              
826             =head2 $client->request
827              
828             =head2 $client->req
829              
830             An accessor for an L<HTTP::Request> object, which was used in the previous action.
831              
832              
833             =head2 $client->response
834              
835             =head2 $client->res
836              
837             An accessor for an L<HTTP::Response> object, which was used in the previous action.
838              
839              
840             =head1 INTERNAL INTERFACES
841              
842             =head2 $client->init
843              
844             =head2 $client->ua
845              
846             Accessor to the UserAgent.
847              
848             =head2 $client->info
849              
850             An accessor to information of Collections described in a Service Document.
851              
852             =head2 $client->cache
853              
854             An accessor to the resource cache.
855              
856             =head2 $client->munge_request($req)
857              
858             =head2 $client->_clear
859              
860             =head2 $client->_get_service(\%args)
861              
862             =head2 $client->_get_categories(\%args)
863              
864             =head2 $client->_get_feed(\%args)
865              
866             =head2 $client->_create_resource(\%args)
867              
868             =head2 $client->_get_resource(\%args)
869              
870             =head2 $client->_update_resource(\%args)
871              
872             =head2 $client->_delete_resource(\%args)
873              
874              
875             =head1 ERROR HANDLING
876              
877             Methods return C<undef> on error, and the error message can be retrieved
878             using the I<errstr> method.
879              
880              
881             =head1 SEE ALSO
882              
883             L<XML::Atom>
884             L<XML::Atom::Service>
885             L<Atompub>
886              
887              
888             =head1 AUTHOR
889              
890             Takeru INOUE, E<lt>takeru.inoue _ gmail.comE<gt>
891              
892              
893             =head1 LICENCE AND COPYRIGHT
894              
895             Copyright (c) 2007, Takeru INOUE C<< <takeru.inoue _ gmail.com> >>. All rights reserved.
896              
897             This module is free software; you can redistribute it and/or
898             modify it under the same terms as Perl itself. See L<perlartistic>.
899              
900              
901             =head1 DISCLAIMER OF WARRANTY
902              
903             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
904             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
905             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
906             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
907             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
908             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
909             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
910             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
911             NECESSARY SERVICING, REPAIR, OR CORRECTION.
912              
913             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
914             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
915             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
916             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
917             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
918             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
919             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
920             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
921             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
922             SUCH DAMAGES.
923              
924             =cut