| 1 | package Catalyst::Controller::Atompub::Collection; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | use warnings; |
|---|
| 5 | |
|---|
| 6 | use Atompub::DateTime qw( datetime ); |
|---|
| 7 | use Atompub::MediaType qw( media_type ); |
|---|
| 8 | use Atompub::Util qw( is_acceptable_media_type is_allowed_category ); |
|---|
| 9 | use Catalyst::Utils; |
|---|
| 10 | use File::Slurp; |
|---|
| 11 | use HTTP::Status; |
|---|
| 12 | use NEXT; |
|---|
| 13 | use POSIX qw( strftime ); |
|---|
| 14 | use Text::CSV; |
|---|
| 15 | use Time::HiRes qw( gettimeofday ); |
|---|
| 16 | use URI::Escape; |
|---|
| 17 | use XML::Atom::Entry; |
|---|
| 18 | |
|---|
| 19 | use base qw( Catalyst::Controller::Atompub::Base ); |
|---|
| 20 | |
|---|
| 21 | __PACKAGE__->mk_accessors( qw( edited ) ); |
|---|
| 22 | |
|---|
| 23 | my %COLLECTION_METHOD = ( GET => '_list', |
|---|
| 24 | HEAD => '_list', |
|---|
| 25 | POST => '_create' ); |
|---|
| 26 | my %RESOURCE_METHOD = ( GET => '_read', |
|---|
| 27 | HEAD => '_read', |
|---|
| 28 | POST => '_create', |
|---|
| 29 | PUT => '_update', |
|---|
| 30 | DELETE => '_delete' ); |
|---|
| 31 | |
|---|
| 32 | sub auto :Private { |
|---|
| 33 | my ( $self, $c ) = @_; |
|---|
| 34 | $self->{resources} = []; |
|---|
| 35 | 1; |
|---|
| 36 | } |
|---|
| 37 | |
|---|
| 38 | # access to the collection |
|---|
| 39 | sub default :Private { |
|---|
| 40 | my ( $self, $c ) = @_; |
|---|
| 41 | my $method = $COLLECTION_METHOD{ uc $c->req->method }; |
|---|
| 42 | if ( ! $method ) { |
|---|
| 43 | $c->res->headers->allow('GET, HEAD, POST'); |
|---|
| 44 | return $self->error( $c, RC_METHOD_NOT_ALLOWED ); |
|---|
| 45 | } |
|---|
| 46 | $self->$method( $c ); |
|---|
| 47 | } |
|---|
| 48 | |
|---|
| 49 | sub edit_uri :LocalRegex('^([^-/?&#][^/?&#]*)') { |
|---|
| 50 | my ( $self, $c ) = @_; |
|---|
| 51 | my $method = $RESOURCE_METHOD{ uc $c->req->method }; |
|---|
| 52 | if ( ! $method ) { |
|---|
| 53 | $c->res->headers->allow('GET, HEAD, PUT, DELETE'); |
|---|
| 54 | return $self->error( $c, RC_METHOD_NOT_ALLOWED ); |
|---|
| 55 | } |
|---|
| 56 | $self->$method( $c ); |
|---|
| 57 | } |
|---|
| 58 | |
|---|
| 59 | sub resource { |
|---|
| 60 | my $self = shift; |
|---|
| 61 | if ( @_ ) { |
|---|
| 62 | my $rc = shift; |
|---|
| 63 | push @{ $self->{resources} }, $rc; |
|---|
| 64 | } |
|---|
| 65 | else { |
|---|
| 66 | my ( $rc ) = grep { $_ } @{ $self->{resources} }; |
|---|
| 67 | return $rc; |
|---|
| 68 | } |
|---|
| 69 | } |
|---|
| 70 | |
|---|
| 71 | *rc = \&resource; |
|---|
| 72 | |
|---|
| 73 | my @ACCESSORS = ( [ 'collection_resource', 'feed', 'is_collection' ], |
|---|
| 74 | [ 'entry_resource', 'entry', 'is_entry' ], |
|---|
| 75 | [ 'media_resource', undef, 'is_media' ], |
|---|
| 76 | [ 'media_link_entry', 'entry', 'is_entry' ] ); |
|---|
| 77 | |
|---|
| 78 | for my $accessor ( @ACCESSORS ) { |
|---|
| 79 | no strict 'refs'; ## no critic |
|---|
| 80 | my ( $method, $type, $is ) = @$accessor; |
|---|
| 81 | *{$method} = sub { |
|---|
| 82 | my $self = shift; |
|---|
| 83 | if ( @_ ) { |
|---|
| 84 | my $rc = $_[0]; |
|---|
| 85 | $rc->type( media_type( $type ) ) if $type; |
|---|
| 86 | push @{ $self->{resources} }, $rc; |
|---|
| 87 | } |
|---|
| 88 | else { |
|---|
| 89 | my ( $rc ) = grep { ! $_->type || $_->$is } |
|---|
| 90 | grep { $_ } |
|---|
| 91 | @{ $self->{resources} }; |
|---|
| 92 | return $rc; |
|---|
| 93 | } |
|---|
| 94 | }; |
|---|
| 95 | } |
|---|
| 96 | |
|---|
| 97 | sub make_edit_uri { |
|---|
| 98 | my ( $self, $c, @args ) = @_; |
|---|
| 99 | |
|---|
| 100 | my $collection_uri = $self->info->get( $c, $self )->href; |
|---|
| 101 | |
|---|
| 102 | my $basename; |
|---|
| 103 | if ( my $slug = $c->req->slug ) { |
|---|
| 104 | my $slug = uri_unescape $slug; |
|---|
| 105 | $slug =~ s/^\s+//; $slug =~ s/\s+$//; $slug =~ s/[.\s]+/_/; |
|---|
| 106 | $basename = uri_escape lc $slug; |
|---|
| 107 | } |
|---|
| 108 | else { |
|---|
| 109 | my ( $sec, $usec ) = gettimeofday; |
|---|
| 110 | $basename |
|---|
| 111 | = join '-', strftime( '%Y%m%d-%H%M%S', localtime($sec) ), sprintf( '%06d', $usec ); |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | my @media_types = map { media_type($_) } ( 'entry', @args ); |
|---|
| 115 | |
|---|
| 116 | my @uris; |
|---|
| 117 | for my $media_type ( @media_types ) { |
|---|
| 118 | my $ext = $media_type->extension || 'bin'; |
|---|
| 119 | my $name = join '.', $basename, $ext; |
|---|
| 120 | push @uris, join '/', $collection_uri, $name; |
|---|
| 121 | } |
|---|
| 122 | |
|---|
| 123 | return wantarray ? @uris : $uris[0]; |
|---|
| 124 | } |
|---|
| 125 | |
|---|
| 126 | for my $operation qw( list create read update delete ) { |
|---|
| 127 | no strict 'refs'; ## no critic |
|---|
| 128 | *{ "do_$operation" } = sub { |
|---|
| 129 | my ( $self, $c, @args ) = @_; |
|---|
| 130 | return $self->error( $c, RC_METHOD_NOT_ALLOWED ) |
|---|
| 131 | unless UNIVERSAL::isa( $self->{handler}{ $operation }, 'CODE' ); |
|---|
| 132 | $self->{handler}{ $operation }( $self, $c, @args ); |
|---|
| 133 | }; |
|---|
| 134 | } |
|---|
| 135 | |
|---|
| 136 | sub _list { |
|---|
| 137 | my ( $self, $c ) = @_; |
|---|
| 138 | |
|---|
| 139 | my $feed = XML::Atom::Feed->new; |
|---|
| 140 | my $title = $self->info->get( $c, $self )->title; |
|---|
| 141 | $feed->title( $title ); |
|---|
| 142 | |
|---|
| 143 | if ( $self->{author} ) { |
|---|
| 144 | my $author = XML::Atom::Person->new; |
|---|
| 145 | $self->{author}{$_} and $author->$_( $self->{author}{$_} ) |
|---|
| 146 | for qw( name email uri ); |
|---|
| 147 | $feed->author( $author ); |
|---|
| 148 | } |
|---|
| 149 | |
|---|
| 150 | $feed->updated( datetime->w3c ); |
|---|
| 151 | |
|---|
| 152 | my $uri = $self->info->get( $c, $self )->href; |
|---|
| 153 | |
|---|
| 154 | $feed->id( $uri ); |
|---|
| 155 | $feed->self_link( $uri ); |
|---|
| 156 | |
|---|
| 157 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 158 | $rc->uri( $uri ); |
|---|
| 159 | $rc->body( $feed ); |
|---|
| 160 | $self->collection_resource( $rc ); |
|---|
| 161 | |
|---|
| 162 | $self->do_list( $c ) |
|---|
| 163 | || return $self->error( $c, RC_INTERNAL_SERVER_ERROR, "Cannot list collection: $uri" ); |
|---|
| 164 | |
|---|
| 165 | if ( ! $c->res->content_type ) { |
|---|
| 166 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content-Type not assigned' ) |
|---|
| 167 | if ! $self->collection_resource || ! $self->collection_resource->type; |
|---|
| 168 | $c->res->content_type( $self->collection_resource->type ); |
|---|
| 169 | } |
|---|
| 170 | |
|---|
| 171 | return if length $c->res->body; |
|---|
| 172 | |
|---|
| 173 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content body not found' ) |
|---|
| 174 | if ! $self->collection_resource || ! $self->collection_resource->body; |
|---|
| 175 | |
|---|
| 176 | $c->res->body( $self->collection_resource->serialize ); |
|---|
| 177 | } |
|---|
| 178 | |
|---|
| 179 | sub _create { |
|---|
| 180 | my ( $self, $c ) = @_; |
|---|
| 181 | |
|---|
| 182 | my $media_type |
|---|
| 183 | = media_type( $c->req->content_type ) || media_type('application/octet-stream'); |
|---|
| 184 | |
|---|
| 185 | my $coll = $self->info->get( $c, $self ); |
|---|
| 186 | |
|---|
| 187 | return $self->error( $c, RC_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type: $media_type" ) |
|---|
| 188 | unless is_acceptable_media_type( $coll, $media_type ); |
|---|
| 189 | |
|---|
| 190 | $self->edited( datetime ); |
|---|
| 191 | |
|---|
| 192 | if ( $media_type->is_a('entry') ) { |
|---|
| 193 | my ( $uri ) = $self->make_edit_uri( $c ); |
|---|
| 194 | |
|---|
| 195 | my $entry = XML::Atom::Entry->new( $c->req->body ) |
|---|
| 196 | || return $self->error( $c, RC_BAD_REQUEST, XML::Atom::Entry->errstr ); |
|---|
| 197 | |
|---|
| 198 | return $self->error( $c, RC_BAD_REQUEST, 'Forbidden category' ) |
|---|
| 199 | unless is_allowed_category( $coll, $entry->category ); |
|---|
| 200 | |
|---|
| 201 | $entry->edited( $self->edited->w3c ); |
|---|
| 202 | $entry->updated( $self->edited->w3c ) unless $entry->updated; |
|---|
| 203 | |
|---|
| 204 | $entry->id( $uri ); |
|---|
| 205 | |
|---|
| 206 | $entry->edit_link( $uri ); |
|---|
| 207 | |
|---|
| 208 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 209 | $rc->edited( $self->edited ); # XXX DEPRECATED |
|---|
| 210 | $rc->uri( $uri ); |
|---|
| 211 | $rc->body( $entry ); |
|---|
| 212 | $self->entry_resource( $rc ); |
|---|
| 213 | } |
|---|
| 214 | else { |
|---|
| 215 | my ( $entry_uri, $media_uri ) = $self->make_edit_uri( $c, $media_type ); |
|---|
| 216 | |
|---|
| 217 | return $self->error( $c, RC_BAD_REQUEST, 'No body' ) |
|---|
| 218 | unless $c->req->body; |
|---|
| 219 | |
|---|
| 220 | my $media |
|---|
| 221 | = read_file( $c->req->body, binmode => ':raw', err_mode => 'carp' ) |
|---|
| 222 | || return $self->error( $c, RC_BAD_REQUEST, 'No media resource' ); |
|---|
| 223 | |
|---|
| 224 | my $entry = XML::Atom::Entry->new; |
|---|
| 225 | |
|---|
| 226 | $entry->edited( $self->edited->w3c ); |
|---|
| 227 | $entry->updated( $self->edited->w3c ) unless $entry->updated; |
|---|
| 228 | |
|---|
| 229 | my $link = XML::Atom::Link->new; |
|---|
| 230 | $link->rel('edit-media'); |
|---|
| 231 | $link->href( $media_uri ); |
|---|
| 232 | $entry->add_link( $link ); |
|---|
| 233 | |
|---|
| 234 | $entry->title( uri_unescape $c->req->slug || 'No Title' ); |
|---|
| 235 | |
|---|
| 236 | my $content = XML::Atom::Content->new; |
|---|
| 237 | $content->src( $media_uri ); |
|---|
| 238 | $content->type( $c->req->content_type ); |
|---|
| 239 | $entry->content( $content ); |
|---|
| 240 | |
|---|
| 241 | $entry->summary(' '); |
|---|
| 242 | |
|---|
| 243 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 244 | $rc->edited( $self->edited ); # XXX DEPRECATED |
|---|
| 245 | $rc->uri( $media_uri ); |
|---|
| 246 | $rc->body( $media ); |
|---|
| 247 | $rc->type( $media_type ); |
|---|
| 248 | $self->media_resource( $rc ); |
|---|
| 249 | |
|---|
| 250 | $entry->id( $entry_uri ); |
|---|
| 251 | |
|---|
| 252 | $link = XML::Atom::Link->new; |
|---|
| 253 | $link->rel('edit'); |
|---|
| 254 | $link->href( $entry_uri ); |
|---|
| 255 | $entry->add_link( $link ); |
|---|
| 256 | |
|---|
| 257 | $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 258 | $rc->edited( $self->edited ); # XXX DEPRECATED |
|---|
| 259 | $rc->uri( $entry_uri ); |
|---|
| 260 | $rc->body( $entry ); |
|---|
| 261 | $self->media_link_entry( $rc ); |
|---|
| 262 | } |
|---|
| 263 | |
|---|
| 264 | $self->do_create( $c ) |
|---|
| 265 | || return $self->error( $c, RC_INTERNAL_SERVER_ERROR, |
|---|
| 266 | 'Cannot create new resource: ' . $self->info->get( $c, $self )->href ); |
|---|
| 267 | |
|---|
| 268 | $c->res->status( RC_CREATED ); |
|---|
| 269 | |
|---|
| 270 | if ( ! $c->res->location ) { |
|---|
| 271 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Location not found' ) |
|---|
| 272 | if ! $self->entry_resource || ! $self->entry_resource->uri; |
|---|
| 273 | $c->res->location( $self->entry_resource->uri ); |
|---|
| 274 | } |
|---|
| 275 | |
|---|
| 276 | if ( ! defined $c->res->etag || ! $c->res->last_modified ) { |
|---|
| 277 | my %ret = $self->find_version( $c, $c->res->location ); |
|---|
| 278 | $c->res->etag( $ret{etag} ) |
|---|
| 279 | if ! defined $c->res->etag && defined $ret{etag}; |
|---|
| 280 | $c->res->last_modified( $ret{last_modified} ) |
|---|
| 281 | if ! $c->res->last_modified && $ret{last_modified}; |
|---|
| 282 | } |
|---|
| 283 | |
|---|
| 284 | if ( ! $c->res->content_type ) { |
|---|
| 285 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content-Type not assigned' ) |
|---|
| 286 | if ! $self->entry_resource || ! $self->entry_resource->type; |
|---|
| 287 | $c->res->content_type( $self->entry_resource->type ); |
|---|
| 288 | } |
|---|
| 289 | |
|---|
| 290 | return if length $c->res->body; |
|---|
| 291 | |
|---|
| 292 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content body not found' ) |
|---|
| 293 | if ! $self->entry_resource || ! $self->entry_resource->body; |
|---|
| 294 | |
|---|
| 295 | $c->res->body( $self->entry_resource->serialize ); |
|---|
| 296 | } |
|---|
| 297 | |
|---|
| 298 | sub _read { |
|---|
| 299 | my ( $self, $c ) = @_; |
|---|
| 300 | |
|---|
| 301 | return $c->res->status( RC_NOT_MODIFIED ) unless $self->_is_modified( $c ); |
|---|
| 302 | |
|---|
| 303 | my $uri = $c->req->uri->no_query; |
|---|
| 304 | |
|---|
| 305 | my @accepts = $self->info->get( $c, $self )->accepts; |
|---|
| 306 | my $media_type = @accepts == 0 ? media_type('entry') |
|---|
| 307 | : @accepts == 1 ? media_type( $accepts[0] ) |
|---|
| 308 | : undef; |
|---|
| 309 | |
|---|
| 310 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 311 | $rc->uri( $uri ); |
|---|
| 312 | $rc->type( $media_type ); |
|---|
| 313 | $self->rc( $rc ); |
|---|
| 314 | |
|---|
| 315 | $self->do_read( $c ) |
|---|
| 316 | || return $self->error( $c, RC_INTERNAL_SERVER_ERROR, "Cannot read resource: $uri" ); |
|---|
| 317 | |
|---|
| 318 | if ( ! defined $c->res->etag || ! $c->res->last_modified ) { |
|---|
| 319 | my %ret = $self->find_version( $c, $uri ); |
|---|
| 320 | $c->res->etag( $ret{etag} ) |
|---|
| 321 | if ! defined $c->res->etag && defined $ret{etag}; |
|---|
| 322 | $c->res->last_modified( $ret{last_modified} ) |
|---|
| 323 | if ! $c->res->last_modified && $ret{last_modified}; |
|---|
| 324 | } |
|---|
| 325 | |
|---|
| 326 | if ( ! $c->res->content_type ) { |
|---|
| 327 | $self->rc->type( media_type('entry') ) |
|---|
| 328 | if UNIVERSAL::isa( $self->rc->body, 'XML::Atom::Entry' ); |
|---|
| 329 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content-Type not assigned' ) |
|---|
| 330 | if ! $self->rc || ! $self->rc->type; |
|---|
| 331 | $c->res->content_type( $self->rc->type ); |
|---|
| 332 | } |
|---|
| 333 | |
|---|
| 334 | return if length $c->res->body; |
|---|
| 335 | |
|---|
| 336 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content body not found' ) |
|---|
| 337 | if ! $self->rc || ! $self->rc->body; |
|---|
| 338 | |
|---|
| 339 | $c->res->body( $self->rc->serialize ); |
|---|
| 340 | } |
|---|
| 341 | |
|---|
| 342 | sub _update { |
|---|
| 343 | my ( $self, $c ) = @_; |
|---|
| 344 | |
|---|
| 345 | return $self->error( $c, RC_PRECONDITION_FAILED ) |
|---|
| 346 | if $self->_is_modified( $c ); |
|---|
| 347 | |
|---|
| 348 | my $media_type |
|---|
| 349 | = media_type( $c->req->content_type ) || media_type('application/octet-stream'); |
|---|
| 350 | |
|---|
| 351 | my $coll = $self->info->get( $c, $self ); |
|---|
| 352 | |
|---|
| 353 | return $self->error( $c, RC_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type: $media_type" ) |
|---|
| 354 | if ! is_acceptable_media_type( $coll, $media_type ) |
|---|
| 355 | && ! $media_type->is_a('entry'); |
|---|
| 356 | # XXX Entries are okay, this is because Media Link Entries can be PUT |
|---|
| 357 | # even to the Media Resource Collection |
|---|
| 358 | |
|---|
| 359 | $self->edited( datetime ); |
|---|
| 360 | |
|---|
| 361 | my $uri = $c->req->uri->no_query; |
|---|
| 362 | |
|---|
| 363 | my $content; |
|---|
| 364 | if ( $media_type->is_a('entry') ) { |
|---|
| 365 | my $entry = XML::Atom::Entry->new( $c->req->body ) |
|---|
| 366 | || return $self->error( $c, RC_BAD_REQUEST, XML::Atom::Entry->errstr ); |
|---|
| 367 | |
|---|
| 368 | return $self->error( $c, RC_BAD_REQUEST, 'Forbidden category' ) |
|---|
| 369 | unless is_allowed_category( $coll, $entry->category ); |
|---|
| 370 | |
|---|
| 371 | $entry->edited( $self->edited->w3c ); |
|---|
| 372 | $entry->updated( $self->edited->w3c ) unless $entry->updated; |
|---|
| 373 | |
|---|
| 374 | $entry->id( $uri ); |
|---|
| 375 | |
|---|
| 376 | $entry->edit_link( $uri ); |
|---|
| 377 | |
|---|
| 378 | # XXX check edit-media |
|---|
| 379 | |
|---|
| 380 | $content = $entry; |
|---|
| 381 | |
|---|
| 382 | $media_type = media_type('entry'); |
|---|
| 383 | } |
|---|
| 384 | else { |
|---|
| 385 | $content |
|---|
| 386 | = read_file( $c->req->body, binmode => ':raw', err_mode => 'carp' ) |
|---|
| 387 | || return $self->error( $c, RC_BAD_REQUEST, 'No media' ); |
|---|
| 388 | } |
|---|
| 389 | |
|---|
| 390 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 391 | $rc->edited( $self->edited ); # XXX DEPRECATED |
|---|
| 392 | $rc->uri( $uri ); |
|---|
| 393 | $rc->body( $content ); |
|---|
| 394 | $rc->type( $media_type ); |
|---|
| 395 | $self->rc( $rc ); |
|---|
| 396 | |
|---|
| 397 | $self->do_update( $c ) |
|---|
| 398 | || return $self->error( $c, RC_INTERNAL_SERVER_ERROR, "Cannot update resource: $uri" ); |
|---|
| 399 | |
|---|
| 400 | if ( ! defined $c->res->etag || ! $c->res->last_modified ) { |
|---|
| 401 | my %ret = $self->find_version( $c, $uri ); |
|---|
| 402 | $c->res->etag( $ret{etag} ) |
|---|
| 403 | if ! defined $c->res->etag && defined $ret{etag}; |
|---|
| 404 | $c->res->last_modified( $ret{last_modified} ) |
|---|
| 405 | if ! $c->res->last_modified && $ret{last_modified}; |
|---|
| 406 | } |
|---|
| 407 | |
|---|
| 408 | if ( ! $c->res->content_type ) { |
|---|
| 409 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content-Type not assigned' ) |
|---|
| 410 | if ! $self->rc || ! $self->rc->type; |
|---|
| 411 | $c->res->content_type( $self->rc->type ); |
|---|
| 412 | } |
|---|
| 413 | |
|---|
| 414 | return if length $c->res->body; |
|---|
| 415 | |
|---|
| 416 | return $self->error( $c, RC_INTERNAL_SERVER_ERROR, 'Content body not found' ) |
|---|
| 417 | if ! $self->rc || ! $self->rc->body; |
|---|
| 418 | |
|---|
| 419 | $c->res->body( $self->rc->serialize ); |
|---|
| 420 | } |
|---|
| 421 | |
|---|
| 422 | sub _delete { |
|---|
| 423 | my ( $self, $c ) = @_; |
|---|
| 424 | |
|---|
| 425 | # If-Match nor If-Unmodified-Since header is not required on DELETE |
|---|
| 426 | # return $self->error( $c, RC_PRECONDITION_FAILED ) |
|---|
| 427 | # if $self->_is_modified( $c ); |
|---|
| 428 | |
|---|
| 429 | my $uri = $c->req->uri->no_query; |
|---|
| 430 | |
|---|
| 431 | my $rc = Catalyst::Controller::Atompub::Collection::Resource->new; |
|---|
| 432 | $rc->uri( $uri ); |
|---|
| 433 | $self->rc( $rc ); |
|---|
| 434 | |
|---|
| 435 | $c->res->status( RC_NO_CONTENT ); |
|---|
| 436 | |
|---|
| 437 | $self->do_delete( $c ) |
|---|
| 438 | || return $self->error( $c, RC_INTERNAL_SERVER_ERROR, "Cannot delete resource: $uri" ); |
|---|
| 439 | } |
|---|
| 440 | |
|---|
| 441 | sub find_version { } |
|---|
| 442 | |
|---|
| 443 | sub _is_modified { |
|---|
| 444 | my ( $self, $c ) = @_; |
|---|
| 445 | |
|---|
| 446 | my $method = $c->req->method; |
|---|
| 447 | |
|---|
| 448 | my %ret = $self->find_version( $c, $c->req->uri->no_query ); |
|---|
| 449 | |
|---|
| 450 | my $etag = $ret{etag}; |
|---|
| 451 | my $last_modified = $ret{last_modified}; |
|---|
| 452 | |
|---|
| 453 | return $method eq 'GET' ? 1 : 0 |
|---|
| 454 | if ! defined $etag && ! $last_modified; # if don't check version |
|---|
| 455 | |
|---|
| 456 | my $match = $method eq 'GET' ? $c->req->if_none_match : $c->req->if_match; |
|---|
| 457 | $match =~ s/^['"](.+)['"]$/$1/ if $match; #" unquote |
|---|
| 458 | |
|---|
| 459 | return 1 if defined $etag && ( ! defined $match || $etag ne $match ); |
|---|
| 460 | |
|---|
| 461 | my $since = $method eq 'GET' ? $c->req->if_modified_since : $c->req->if_unmodified_since; |
|---|
| 462 | |
|---|
| 463 | return 1 if $last_modified && ( ! $since || datetime( $last_modified ) != datetime( $since ) ); |
|---|
| 464 | |
|---|
| 465 | return 0; |
|---|
| 466 | } |
|---|
| 467 | |
|---|
| 468 | sub create_action { |
|---|
| 469 | my $self = shift; |
|---|
| 470 | my %args = @_; # namespace, name, reverse, class, attributes, code |
|---|
| 471 | |
|---|
| 472 | my $attr = lc $args{attributes}{Atompub}[0]; |
|---|
| 473 | my $code = $args{code}; |
|---|
| 474 | |
|---|
| 475 | my $csv = Text::CSV->new( { allow_whitespace => 1 } ); |
|---|
| 476 | $csv->parse( $attr ); |
|---|
| 477 | for ( $csv->fields ) { |
|---|
| 478 | $self->{handler}{ $_ } = $code if length $_; |
|---|
| 479 | # %args = (); # removes 'Loaded Private actions' message in initialization |
|---|
| 480 | } |
|---|
| 481 | |
|---|
| 482 | $self->NEXT::create_action( %args ); |
|---|
| 483 | } |
|---|
| 484 | |
|---|
| 485 | sub URI::no_query { [ split /[?&]/, shift->canonical ]->[0] } |
|---|
| 486 | |
|---|
| 487 | package Catalyst::Controller::Atompub::Collection::Resource; |
|---|
| 488 | |
|---|
| 489 | use Atompub::MediaType qw( media_type ); |
|---|
| 490 | |
|---|
| 491 | use base qw( Class::Accessor::Fast ); |
|---|
| 492 | |
|---|
| 493 | __PACKAGE__->mk_accessors( qw( uri type body ) ); |
|---|
| 494 | |
|---|
| 495 | sub edited { |
|---|
| 496 | my $self = shift; |
|---|
| 497 | if (@_) { |
|---|
| 498 | $self->{edited} = $_[0]; |
|---|
| 499 | } |
|---|
| 500 | else { |
|---|
| 501 | warn '$collection->$resource->edited is DEPRECATED, and see $collection->edited'; |
|---|
| 502 | return $self->{edited}; |
|---|
| 503 | } |
|---|
| 504 | } |
|---|
| 505 | |
|---|
| 506 | sub is_collection { |
|---|
| 507 | my $self = shift; |
|---|
| 508 | my $media_type = media_type( $self->type ) || return; |
|---|
| 509 | return $media_type->is_a('feed'); |
|---|
| 510 | } |
|---|
| 511 | |
|---|
| 512 | sub is_entry { |
|---|
| 513 | my $self = shift; |
|---|
| 514 | my $media_type = media_type( $self->type ) || return; |
|---|
| 515 | return $media_type->is_a('entry'); |
|---|
| 516 | } |
|---|
| 517 | |
|---|
| 518 | sub is_media { |
|---|
| 519 | my $self = shift; |
|---|
| 520 | my $media_type = media_type( $self->type ) || return; |
|---|
| 521 | return ! $media_type->is_a('application/atom+xml'); |
|---|
| 522 | } |
|---|
| 523 | |
|---|
| 524 | sub serialize { |
|---|
| 525 | my $self = shift; |
|---|
| 526 | my $body = $self->body; |
|---|
| 527 | UNIVERSAL::can( $body, 'as_xml' ) ? $body->as_xml : $body; |
|---|
| 528 | } |
|---|
| 529 | |
|---|
| 530 | 1; |
|---|
| 531 | __END__ |
|---|
| 532 | |
|---|
| 533 | =head1 NAME |
|---|
| 534 | |
|---|
| 535 | Catalyst::Controller::Atompub::Collection |
|---|
| 536 | - A Catalyst controller for the Atom Collection Resources |
|---|
| 537 | |
|---|
| 538 | |
|---|
| 539 | =head1 SYNOPSIS |
|---|
| 540 | |
|---|
| 541 | # Use the Catalyst helper |
|---|
| 542 | $ perl script/myatom_create.pl controller MyCollection Atompub::Collection |
|---|
| 543 | |
|---|
| 544 | # And edit lib/MyAtom/Controller/MyCollection.pm |
|---|
| 545 | package MyAtom::Controller::MyCollection; |
|---|
| 546 | use base 'Catalyst::Controller::Atompub::Collection'; |
|---|
| 547 | |
|---|
| 548 | # List resources in a Feed Document, which must be implemented in |
|---|
| 549 | # the mehtod with "Atompub(list)" attribute |
|---|
| 550 | sub get_feed :Atompub(list) { |
|---|
| 551 | my ( $self, $c ) = @_; |
|---|
| 552 | |
|---|
| 553 | # Skeleton of the Feed (XML::Atom::Feed) was prepared by |
|---|
| 554 | # C::C::Atompub |
|---|
| 555 | my $feed = $self->collection_resource->body; |
|---|
| 556 | |
|---|
| 557 | # Retrieve Entries sorted in descending order |
|---|
| 558 | my $rs = $c->model('DBIC::Entries') |
|---|
| 559 | ->search( {}, { order_by => 'edited desc' } ); |
|---|
| 560 | |
|---|
| 561 | # Add Entries to the Feed |
|---|
| 562 | while ( my $entry_resource = $rs->next ) { |
|---|
| 563 | my $entry = XML::Atom::Entry->new( \$entry_resource->xml ); |
|---|
| 564 | $feed->add_entry( $entry ); |
|---|
| 565 | } |
|---|
| 566 | |
|---|
| 567 | # Return true on success |
|---|
| 568 | return 1; |
|---|
| 569 | } |
|---|
| 570 | |
|---|
| 571 | # Create new Entry in the method with "Atompub(create)" attribute |
|---|
| 572 | sub create_entry :Atompub(create) { |
|---|
| 573 | my ( $self, $c ) = @_; |
|---|
| 574 | |
|---|
| 575 | # URI of the new Entry, which was determined by C::C::Atompub |
|---|
| 576 | my $uri = $self->entry_resource->uri; |
|---|
| 577 | |
|---|
| 578 | # app:edited element, which was assigned by C::C::Atompub, |
|---|
| 579 | # is coverted into ISO 8601 format like '2007-01-01 00:00:00' |
|---|
| 580 | my $edited = $self->edited->iso; |
|---|
| 581 | |
|---|
| 582 | # POSTed Entry (XML::Atom::Entry) |
|---|
| 583 | my $entry = $self->entry_resource->body; |
|---|
| 584 | |
|---|
| 585 | # Create new Entry |
|---|
| 586 | $c->model('DBIC::Entries')->create( { |
|---|
| 587 | uri => $uri, |
|---|
| 588 | edited => $edited, |
|---|
| 589 | xml => $entry->as_xml, |
|---|
| 590 | } ); |
|---|
| 591 | |
|---|
| 592 | # Return true on success |
|---|
| 593 | return 1; |
|---|
| 594 | } |
|---|
| 595 | |
|---|
| 596 | # Search the requested Entry in the method with "Atompub(read)" |
|---|
| 597 | # attribute |
|---|
| 598 | sub get_entry :Atompub(read) { |
|---|
| 599 | my ( $self, $c ) = @_; |
|---|
| 600 | |
|---|
| 601 | my $uri = $c->entry_resource->uri; |
|---|
| 602 | |
|---|
| 603 | # Retrieve the Entry |
|---|
| 604 | my $rs = $c->model('DBIC::Entries')->find( { uri => $uri } ); |
|---|
| 605 | |
|---|
| 606 | # Set the Entry |
|---|
| 607 | my $entry = XML::Atom::Entry->new( \$rs->xml ); |
|---|
| 608 | $self->entry_resource->body( $entry ); |
|---|
| 609 | |
|---|
| 610 | # Return true on success |
|---|
| 611 | return 1; |
|---|
| 612 | } |
|---|
| 613 | |
|---|
| 614 | # Update the requested Entry in the method with "Atompub(update)" |
|---|
| 615 | # attribute |
|---|
| 616 | sub update_entry :Atompub(update) { |
|---|
| 617 | my ( $self, $c ) = @_; |
|---|
| 618 | |
|---|
| 619 | my $uri = $c->entry_resource->uri; |
|---|
| 620 | |
|---|
| 621 | # app:edited element, which was assigned by C::C::Atompub, |
|---|
| 622 | # is coverted into ISO 8601 format like '2007-01-01 00:00:00' |
|---|
| 623 | my $edited = $self->edited->iso; |
|---|
| 624 | |
|---|
| 625 | # PUTted Entry (XML::Atom::Entry) |
|---|
| 626 | my $entry = $self->entry_resource->body; |
|---|
| 627 | |
|---|
| 628 | # Update the Entry |
|---|
| 629 | $c->model('DBIC::Entries')->find( { uri => $uri } ) |
|---|
| 630 | ->update( { |
|---|
| 631 | uri => $uri, |
|---|
| 632 | edited => $edited, |
|---|
| 633 | xml => $entry->as_xml, |
|---|
| 634 | } ); |
|---|
| 635 | |
|---|
| 636 | # Return true on success |
|---|
| 637 | return 1; |
|---|
| 638 | } |
|---|
| 639 | |
|---|
| 640 | # Delete the requested Entry in the method with "Atompub(delete)" |
|---|
| 641 | # attribute |
|---|
| 642 | sub delete_entry :Atompub(delete) { |
|---|
| 643 | my ( $self, $c ) = @_; |
|---|
| 644 | |
|---|
| 645 | my $uri = $c->entry_resource->uri; |
|---|
| 646 | |
|---|
| 647 | # Delete the Entry |
|---|
| 648 | $c->model('DBIC::Entries')->find( { uri => $uri } )->delete; |
|---|
| 649 | |
|---|
| 650 | # Return true on success |
|---|
| 651 | return 1; |
|---|
| 652 | } |
|---|
| 653 | |
|---|
| 654 | # Access to http://localhost:3000/mycollection and get Feed Document |
|---|
| 655 | |
|---|
| 656 | |
|---|
| 657 | =head1 DESCRIPTION |
|---|
| 658 | |
|---|
| 659 | Catalyst::Controller::Atompub::Collection provides the following features: |
|---|
| 660 | |
|---|
| 661 | =over 4 |
|---|
| 662 | |
|---|
| 663 | =item * Pre-processing requests |
|---|
| 664 | |
|---|
| 665 | L<Catalyst::Controller::Atompub::Collection> pre-processes the HTTP requests. |
|---|
| 666 | All you have to do is just writing CRUD operations in the subroutines |
|---|
| 667 | with I<Atompub> attribute. |
|---|
| 668 | |
|---|
| 669 | =item * Media Resource support |
|---|
| 670 | |
|---|
| 671 | Media Resources (binary data) as well as Entry Resources are supported. |
|---|
| 672 | A Media Link Entry, which has an I<atom:link> element to the newly created Media Resource, |
|---|
| 673 | is given by L<Catalyst::Controller::Atompub::Collection>. |
|---|
| 674 | |
|---|
| 675 | =item * Media type check |
|---|
| 676 | |
|---|
| 677 | L<Catalyst::Controller::Atompub::Collection> checks a media type of |
|---|
| 678 | the POSTed/PUTted resource based on collection configuration. |
|---|
| 679 | |
|---|
| 680 | =item * Category check |
|---|
| 681 | |
|---|
| 682 | L<Catalyst::Controller::Atompub::Collection> checks |
|---|
| 683 | I<atom:category> elements in the POSTed/PUTted Entry Document |
|---|
| 684 | based on collection configuration. |
|---|
| 685 | |
|---|
| 686 | =item * Cache controll and versioning |
|---|
| 687 | |
|---|
| 688 | Cache controll and versioning are enabled just by overriding C<find_version> method, |
|---|
| 689 | which returns I<ETag> and/or I<Last-Modified> header. |
|---|
| 690 | |
|---|
| 691 | =item * Naming resources by I<Slug> header |
|---|
| 692 | |
|---|
| 693 | Resource URIs are determined based on I<Slug> header if exists. |
|---|
| 694 | If the I<Slug> header is "Entry 1", the resource URI will be like: |
|---|
| 695 | |
|---|
| 696 | http://localhost:3000/mycollection/entry_1.atom |
|---|
| 697 | |
|---|
| 698 | The default naming rules can be changed by overriding C<make_edit_uri> method. |
|---|
| 699 | |
|---|
| 700 | =back |
|---|
| 701 | |
|---|
| 702 | |
|---|
| 703 | =head1 SUBCLASSING |
|---|
| 704 | |
|---|
| 705 | One or more subclasses are required in your Atompub server implementation. |
|---|
| 706 | In the subclasses, methods with the following attributes must be defined. |
|---|
| 707 | |
|---|
| 708 | |
|---|
| 709 | =head2 sub xxx :Atompub(list) |
|---|
| 710 | |
|---|
| 711 | Lists resources in a Feed Document. |
|---|
| 712 | |
|---|
| 713 | This method is expected to add Entries and other elements to a skeleton of the Feed. |
|---|
| 714 | The following accessors can be used. |
|---|
| 715 | |
|---|
| 716 | =over 2 |
|---|
| 717 | |
|---|
| 718 | =item - $controller->collection_resource->uri |
|---|
| 719 | |
|---|
| 720 | URI of Collection |
|---|
| 721 | |
|---|
| 722 | =item - $controller->collection_resource->body |
|---|
| 723 | |
|---|
| 724 | Skeleton of Feed (L<XML::Atom::Feed>) |
|---|
| 725 | |
|---|
| 726 | =back |
|---|
| 727 | |
|---|
| 728 | Returns true on success, false otherwise. |
|---|
| 729 | |
|---|
| 730 | |
|---|
| 731 | =head2 sub xxx :Atompub(create) |
|---|
| 732 | |
|---|
| 733 | Creates new resource. |
|---|
| 734 | |
|---|
| 735 | =over 4 |
|---|
| 736 | |
|---|
| 737 | =item * In Collections with Entry Resources |
|---|
| 738 | |
|---|
| 739 | The implementation is expected to insert the new Entry to your model, such as L<DBIx::Class>. |
|---|
| 740 | The following accessors can be used. |
|---|
| 741 | |
|---|
| 742 | =over 2 |
|---|
| 743 | |
|---|
| 744 | =item - $controller->entry_resource->uri |
|---|
| 745 | |
|---|
| 746 | URI of New Entry |
|---|
| 747 | |
|---|
| 748 | =item - $controller->entry_resource->edited |
|---|
| 749 | |
|---|
| 750 | I<app:edited> element of New Entry |
|---|
| 751 | |
|---|
| 752 | =item - $controller->entry_resource->body |
|---|
| 753 | |
|---|
| 754 | New Entry (L<XML::Atom::Entry>) |
|---|
| 755 | |
|---|
| 756 | =back |
|---|
| 757 | |
|---|
| 758 | |
|---|
| 759 | =item * In Collections with Media Resources |
|---|
| 760 | |
|---|
| 761 | The implementation is expected to insert new Media Link Entry as well as new Media Resource |
|---|
| 762 | to your model, such as L<DBIx::Class>. |
|---|
| 763 | |
|---|
| 764 | The following accessors can be used for the Media Resource. |
|---|
| 765 | |
|---|
| 766 | =over 2 |
|---|
| 767 | |
|---|
| 768 | =item - $controller->media_resource->uri |
|---|
| 769 | |
|---|
| 770 | URI of New Media Resource |
|---|
| 771 | |
|---|
| 772 | =item - $controller->media_resource->edited |
|---|
| 773 | |
|---|
| 774 | I<app:edited> element of New Media Resource |
|---|
| 775 | |
|---|
| 776 | =item - $controller->media_resource->type |
|---|
| 777 | |
|---|
| 778 | Media type of New Media Resource |
|---|
| 779 | |
|---|
| 780 | =item - $controller->media_resource->body |
|---|
| 781 | |
|---|
| 782 | New Media Resource (a byte string) |
|---|
| 783 | |
|---|
| 784 | =back |
|---|
| 785 | |
|---|
| 786 | |
|---|
| 787 | The following accessors can be used for Media Link Entry. |
|---|
| 788 | |
|---|
| 789 | =over 2 |
|---|
| 790 | |
|---|
| 791 | =item - $controller->media_link_entry->uri |
|---|
| 792 | |
|---|
| 793 | URI of New Media Link Entry |
|---|
| 794 | |
|---|
| 795 | =item - $controller->media_link_entry->edited |
|---|
| 796 | |
|---|
| 797 | I<app:edited> element of New Media Link Entry |
|---|
| 798 | |
|---|
| 799 | =item - $controller->media_link_entry->body |
|---|
| 800 | |
|---|
| 801 | New Media Link Entry (L<XML::Atom::Entry>) |
|---|
| 802 | |
|---|
| 803 | =back |
|---|
| 804 | |
|---|
| 805 | |
|---|
| 806 | =back |
|---|
| 807 | |
|---|
| 808 | Returns true on success, false otherwise. |
|---|
| 809 | |
|---|
| 810 | |
|---|
| 811 | =head2 sub xxx :Atompub(read) |
|---|
| 812 | |
|---|
| 813 | Searchs the requested resource. |
|---|
| 814 | |
|---|
| 815 | =over 4 |
|---|
| 816 | |
|---|
| 817 | =item * In Collections with Entry Resources |
|---|
| 818 | |
|---|
| 819 | The implementation is expected to search the Entry, |
|---|
| 820 | which must be stored in C<body> accessor described below. |
|---|
| 821 | The following accessors can be used. |
|---|
| 822 | |
|---|
| 823 | =over 2 |
|---|
| 824 | |
|---|
| 825 | =item - $controller->entry_resource->body |
|---|
| 826 | |
|---|
| 827 | Entry (L<XML::Atom::Entry>) |
|---|
| 828 | |
|---|
| 829 | =back |
|---|
| 830 | |
|---|
| 831 | =item * In Collections with Media Resources |
|---|
| 832 | |
|---|
| 833 | The implementation is expected to search Media Resource or Media Link Entry, |
|---|
| 834 | which must be stored in C<body> accessor described below. |
|---|
| 835 | The following accessors can be used for the Media Resource. |
|---|
| 836 | |
|---|
| 837 | =over 2 |
|---|
| 838 | |
|---|
| 839 | =item - $controller->media_resource->type |
|---|
| 840 | |
|---|
| 841 | Media type of Media Resource |
|---|
| 842 | |
|---|
| 843 | =item - $controller->media_resource->body |
|---|
| 844 | |
|---|
| 845 | Media Resource (a byte string) |
|---|
| 846 | |
|---|
| 847 | =back |
|---|
| 848 | |
|---|
| 849 | The following accessors can be used for the Media Link Entry. |
|---|
| 850 | |
|---|
| 851 | =over 2 |
|---|
| 852 | |
|---|
| 853 | =item - $controller->media_link_entry->body |
|---|
| 854 | |
|---|
| 855 | Media Link Entry (L<XML::Atom::Entry>) |
|---|
| 856 | |
|---|
| 857 | =back |
|---|
| 858 | |
|---|
| 859 | =back |
|---|
| 860 | |
|---|
| 861 | Returns true on success, false otherwise. |
|---|
| 862 | |
|---|
| 863 | |
|---|
| 864 | =head2 sub xxx :Atompub(update) |
|---|
| 865 | |
|---|
| 866 | Updates the requested resource. |
|---|
| 867 | |
|---|
| 868 | |
|---|
| 869 | =over 4 |
|---|
| 870 | |
|---|
| 871 | =item * In Collections with Entry Resources |
|---|
| 872 | |
|---|
| 873 | The implementation is expected to update the Entry. |
|---|
| 874 | The following accessors can be used. |
|---|
| 875 | |
|---|
| 876 | =over 2 |
|---|
| 877 | |
|---|
| 878 | =item - $controller->entry_resource->uri |
|---|
| 879 | |
|---|
| 880 | URI of Entry |
|---|
| 881 | |
|---|
| 882 | =item - $controller->entry_resource->edited |
|---|
| 883 | |
|---|
| 884 | I<app:edited> element of Entry |
|---|
| 885 | |
|---|
| 886 | =item - $controller->entry_resource->body |
|---|
| 887 | |
|---|
| 888 | Entry (L<XML::Atom::Entry>) |
|---|
| 889 | |
|---|
| 890 | =back |
|---|
| 891 | |
|---|
| 892 | |
|---|
| 893 | =item * In Collections with Media Resources |
|---|
| 894 | |
|---|
| 895 | The implementation is expected to update the Media Resource or the Media Link Entry. |
|---|
| 896 | The following accessors can be used for the Media Resource. |
|---|
| 897 | |
|---|
| 898 | =over 2 |
|---|
| 899 | |
|---|
| 900 | =item - $controller->media_resource->uri |
|---|
| 901 | |
|---|
| 902 | URI of Media Resource |
|---|
| 903 | |
|---|
| 904 | =item - $controller->media_resource->edited |
|---|
| 905 | |
|---|
| 906 | I<app:edited> element of Media Resource |
|---|
| 907 | |
|---|
| 908 | =item - $controller->media_resource->type |
|---|
| 909 | |
|---|
| 910 | Media type of Media Resource |
|---|
| 911 | |
|---|
| 912 | =item - $controller->media_resource->body |
|---|
| 913 | |
|---|
| 914 | Media Resource (a byte string) |
|---|
| 915 | |
|---|
| 916 | =back |
|---|
| 917 | |
|---|
| 918 | |
|---|
| 919 | The following accessors can be used for the Media Link Entry. |
|---|
| 920 | |
|---|
| 921 | =over 2 |
|---|
| 922 | |
|---|
| 923 | =item - $controller->media_link_entry->uri |
|---|
| 924 | |
|---|
| 925 | URI of Media Link Entry |
|---|
| 926 | |
|---|
| 927 | =item - $controller->media_link_entry->edited |
|---|
| 928 | |
|---|
| 929 | I<app:edited> element of Media Link Entry |
|---|
| 930 | |
|---|
| 931 | =item - $controller->media_link_entry->body |
|---|
| 932 | |
|---|
| 933 | Media Link Entry (L<XML::Atom::Entry>) |
|---|
| 934 | |
|---|
| 935 | =back |
|---|
| 936 | |
|---|
| 937 | =back |
|---|
| 938 | |
|---|
| 939 | Returns true on success, false otherwise. |
|---|
| 940 | |
|---|
| 941 | |
|---|
| 942 | =head2 sub xxx :Atompub(delete) |
|---|
| 943 | |
|---|
| 944 | Deletes the requested resource. |
|---|
| 945 | |
|---|
| 946 | The implementation is expected to delete the resource. |
|---|
| 947 | If the collection contains Media Resources, |
|---|
| 948 | corresponding Media Link Entry must be deleted at once. |
|---|
| 949 | |
|---|
| 950 | Returns true on success, false otherwise. |
|---|
| 951 | |
|---|
| 952 | |
|---|
| 953 | =head1 METHODS |
|---|
| 954 | |
|---|
| 955 | The following methods can be overridden to change the default behaviors. |
|---|
| 956 | |
|---|
| 957 | |
|---|
| 958 | =head2 $controller->find_version( $uri ) |
|---|
| 959 | |
|---|
| 960 | By overriding C<find_version> method, cache control and versioning are enabled. |
|---|
| 961 | |
|---|
| 962 | The implementation is expected to return I<ETag> and/or I<Last-Modified> value |
|---|
| 963 | of the requested URI: |
|---|
| 964 | |
|---|
| 965 | package MyAtom::Controller::MyCollection; |
|---|
| 966 | |
|---|
| 967 | sub find_version { |
|---|
| 968 | my ( $self, $c, $uri ) = @_; |
|---|
| 969 | |
|---|
| 970 | # Retrieve ETag and/or Last-Modified of $uri |
|---|
| 971 | |
|---|
| 972 | return ( etag => $etag, last_modified => $last_modified ); |
|---|
| 973 | } |
|---|
| 974 | |
|---|
| 975 | When a resource of the URI does not exist, the implementation must return an empty array. |
|---|
| 976 | |
|---|
| 977 | The behavior of Atompub server will be changed in the following manner: |
|---|
| 978 | |
|---|
| 979 | =over 4 |
|---|
| 980 | |
|---|
| 981 | =item * On GET request |
|---|
| 982 | |
|---|
| 983 | Status code of 304 (Not Modified) will be returned, |
|---|
| 984 | if the requested resource has not been changed. |
|---|
| 985 | |
|---|
| 986 | =item * On PUT request |
|---|
| 987 | |
|---|
| 988 | Status code of 412 (Precondition Failed) will be returned, |
|---|
| 989 | if the current version of the resource that a client is modifying is not |
|---|
| 990 | the same as the version that the client is basing its modifications on. |
|---|
| 991 | |
|---|
| 992 | =back |
|---|
| 993 | |
|---|
| 994 | |
|---|
| 995 | =head2 $controller->make_edit_uri( $c, [ @args ]); |
|---|
| 996 | |
|---|
| 997 | By default, if the I<Slug> header is "Entry 1", the resource URI will be like: |
|---|
| 998 | |
|---|
| 999 | http://localhost:3000/mycollection/entry_1.atom |
|---|
| 1000 | |
|---|
| 1001 | This default behavior can be changed by overriding C<find_version> method: |
|---|
| 1002 | |
|---|
| 1003 | package MyAtom::Controller::MyCollection; |
|---|
| 1004 | |
|---|
| 1005 | sub make_edit_uri { |
|---|
| 1006 | my ( $self, $c, @args ) = @_; |
|---|
| 1007 | |
|---|
| 1008 | my @uris = $self->SUPER::make_edit_uri( $c, @args ); |
|---|
| 1009 | |
|---|
| 1010 | # Modify @uris as you like |
|---|
| 1011 | |
|---|
| 1012 | return @uris; |
|---|
| 1013 | } |
|---|
| 1014 | |
|---|
| 1015 | Arguments @args are media types of POSTed resources. |
|---|
| 1016 | |
|---|
| 1017 | This method returns an array of resource URIs; |
|---|
| 1018 | the first element is a URI of the Entry Resource (including Media Link Entry), |
|---|
| 1019 | and the second one is a URI of the Media Resource if exists. |
|---|
| 1020 | |
|---|
| 1021 | |
|---|
| 1022 | =head2 $controller->do_list |
|---|
| 1023 | |
|---|
| 1024 | =head2 $controller->do_create |
|---|
| 1025 | |
|---|
| 1026 | =head2 $controller->do_read |
|---|
| 1027 | |
|---|
| 1028 | =head2 $controller->do_update |
|---|
| 1029 | |
|---|
| 1030 | =head2 $controller->do_delete |
|---|
| 1031 | |
|---|
| 1032 | |
|---|
| 1033 | =head1 ACCESSORS |
|---|
| 1034 | |
|---|
| 1035 | =head2 $controller->resource |
|---|
| 1036 | |
|---|
| 1037 | =head2 $controller->rc |
|---|
| 1038 | |
|---|
| 1039 | An accessor for a resource object except Media Link Entry. |
|---|
| 1040 | |
|---|
| 1041 | |
|---|
| 1042 | =head2 $controller->collection_resource |
|---|
| 1043 | |
|---|
| 1044 | An accessor for a Collection Resource object. |
|---|
| 1045 | |
|---|
| 1046 | |
|---|
| 1047 | =head2 $controller->entry_resource |
|---|
| 1048 | |
|---|
| 1049 | An accessor for an Entry Resource objecgt. |
|---|
| 1050 | |
|---|
| 1051 | |
|---|
| 1052 | =head2 $controller->media_resource |
|---|
| 1053 | |
|---|
| 1054 | An accessor for a Media Resource object. |
|---|
| 1055 | |
|---|
| 1056 | |
|---|
| 1057 | =head2 $controller->media_link_entry |
|---|
| 1058 | |
|---|
| 1059 | An accessor for a Media Link Entry object. |
|---|
| 1060 | |
|---|
| 1061 | |
|---|
| 1062 | =head2 $controller->edited |
|---|
| 1063 | |
|---|
| 1064 | An accessor for a app:edited, which is applied for the POSTed/PUTted Entry Resource. |
|---|
| 1065 | |
|---|
| 1066 | |
|---|
| 1067 | =head1 INTERNAL INTERFACES |
|---|
| 1068 | |
|---|
| 1069 | =head2 $controller->auto |
|---|
| 1070 | |
|---|
| 1071 | =head2 $controller->default |
|---|
| 1072 | |
|---|
| 1073 | =head2 $controller->edit_uri |
|---|
| 1074 | |
|---|
| 1075 | =head2 $controller->_list |
|---|
| 1076 | |
|---|
| 1077 | =head2 $controller->_create |
|---|
| 1078 | |
|---|
| 1079 | =head2 $controller->_read |
|---|
| 1080 | |
|---|
| 1081 | =head2 $controller->_update |
|---|
| 1082 | |
|---|
| 1083 | =head2 $controller->_delete |
|---|
| 1084 | |
|---|
| 1085 | =head2 $controller->_is_modified |
|---|
| 1086 | |
|---|
| 1087 | =head2 $controller->create_action |
|---|
| 1088 | |
|---|
| 1089 | |
|---|
| 1090 | =head1 FEED PAGING |
|---|
| 1091 | |
|---|
| 1092 | This module does not provide paging of Feed Documents. |
|---|
| 1093 | Paging mechanism should be implemented in a method with "Atompub(list)" attribute. |
|---|
| 1094 | |
|---|
| 1095 | |
|---|
| 1096 | =head1 CONFIGURATION |
|---|
| 1097 | |
|---|
| 1098 | By default (no configuration), Collections accept Entry Documents |
|---|
| 1099 | (application/atom+xml) and any I<atom:category> element. |
|---|
| 1100 | |
|---|
| 1101 | Acceptable I<atom:category> elements can be set like: |
|---|
| 1102 | |
|---|
| 1103 | Controller::EntryCollection: |
|---|
| 1104 | collection: |
|---|
| 1105 | title: Diary |
|---|
| 1106 | categories: |
|---|
| 1107 | - fixed: yes |
|---|
| 1108 | scheme: http://example.com/cats/big3 |
|---|
| 1109 | category: |
|---|
| 1110 | - term: animal |
|---|
| 1111 | label: animal |
|---|
| 1112 | - term: vegetable |
|---|
| 1113 | label: vegetable |
|---|
| 1114 | - term: mineral |
|---|
| 1115 | scheme: http://example.com/dogs/big3 |
|---|
| 1116 | label: mineral |
|---|
| 1117 | |
|---|
| 1118 | Acceptable media types is configured like: |
|---|
| 1119 | |
|---|
| 1120 | Controller::MediaCollection: |
|---|
| 1121 | collection: |
|---|
| 1122 | title: Photo |
|---|
| 1123 | accept: |
|---|
| 1124 | - image/png |
|---|
| 1125 | - image/jpeg |
|---|
| 1126 | - image/gif |
|---|
| 1127 | |
|---|
| 1128 | |
|---|
| 1129 | =head1 ERROR HANDLING |
|---|
| 1130 | |
|---|
| 1131 | See ERROR HANDLING in L<Catalyst::Controller::Atompub::Base>. |
|---|
| 1132 | |
|---|
| 1133 | |
|---|
| 1134 | =head1 SAMPLES |
|---|
| 1135 | |
|---|
| 1136 | See SAMPLES in L<Catalyst::Controller::Atompub>. |
|---|
| 1137 | |
|---|
| 1138 | |
|---|
| 1139 | =head1 SEE ALSO |
|---|
| 1140 | |
|---|
| 1141 | L<XML::Atom> |
|---|
| 1142 | L<XML::Atom::Service> |
|---|
| 1143 | L<Atompub> |
|---|
| 1144 | L<Catalyst::Controller::Atompub> |
|---|
| 1145 | |
|---|
| 1146 | |
|---|
| 1147 | =head1 AUTHOR |
|---|
| 1148 | |
|---|
| 1149 | Takeru INOUE C<< <takeru.inoue _ gmail.com> >> |
|---|
| 1150 | |
|---|
| 1151 | |
|---|
| 1152 | =head1 LICENCE AND COPYRIGHT |
|---|
| 1153 | |
|---|
| 1154 | Copyright (c) 2007, Takeru INOUE C<< <takeru.inoue _ gmail.com> >>. All rights reserved. |
|---|
| 1155 | |
|---|
| 1156 | This module is free software; you can redistribute it and/or |
|---|
| 1157 | modify it under the same terms as Perl itself. See L<perlartistic>. |
|---|
| 1158 | |
|---|
| 1159 | |
|---|
| 1160 | =head1 DISCLAIMER OF WARRANTY |
|---|
| 1161 | |
|---|
| 1162 | BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY |
|---|
| 1163 | FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN |
|---|
| 1164 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES |
|---|
| 1165 | PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER |
|---|
| 1166 | EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|---|
| 1167 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE |
|---|
| 1168 | ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH |
|---|
| 1169 | YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL |
|---|
| 1170 | NECESSARY SERVICING, REPAIR, OR CORRECTION. |
|---|
| 1171 | |
|---|
| 1172 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
|---|
| 1173 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR |
|---|
| 1174 | REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE |
|---|
| 1175 | LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, |
|---|
| 1176 | OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE |
|---|
| 1177 | THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING |
|---|
| 1178 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A |
|---|
| 1179 | FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF |
|---|
| 1180 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
|---|
| 1181 | SUCH DAMAGES. |
|---|