source: bookmarks/trunk/BookmarkController.pm @ 63

Last change on this file since 63 was 63, checked in by peter, 11 years ago
  • renamed controller methods:
    • create --> create_and_redirect
    • edit --> update_and_redirect
  • use Moose instrospection instead of exception handling to determine if a requested field is available on a bookmark
  • removed a debugging response line
File size: 12.7 KB
RevLine 
[59]1package BookmarkController;
2use Moose;
[5]3
4use Encode;
[57]5use HTTP::Date qw{time2isoz time2iso time2str str2time};
[5]6use JSON;
7use Bookmarks;
[7]8use URI;
[59]9use Template;
[7]10
[61]11has dbname => (
12    is => 'ro',
13    required => 1,
14);
[59]15has bookmarks => (
[61]16    is => 'ro',
[59]17    handles => [qw{get_bookmark}],
[61]18    builder => '_build_bookmarks',
19    lazy => 1,
[59]20);
21has base_uri => (
22    is => 'ro',
23    builder => '_build_base_uri',
24    lazy => 1,
25);
26has request => (
27    is => 'ro',
[61]28    required => 1,
[59]29);
30
[61]31sub _build_bookmarks {
32    my $self = shift;
33    return Bookmarks->new({
34        dbname   => $self->dbname,
35        base_uri => $self->base_uri,
36    });
37}
38
[59]39sub _build_base_uri {
[5]40    my $self = shift;
[59]41    my $url = $self->request->base;
[41]42
[53]43    $url .= '/' unless $url =~ m{/$};
[59]44    return URI->new($url);
[5]45}
46
[38]47sub _get_list_links {
48    my $self = shift;
[52]49    my ($self_type, $query) = @_;
[38]50    my @links = (
51        {
52            text => 'JSON',
53            type => 'application/json',
54            query => {
[52]55                %$query,
[38]56                format => 'json',
57            },
58        },
59        {
60            text => 'XBEL',
61            type => 'application/xml',
62            query => {
[52]63                %$query,
[38]64                format => 'xbel',
65            },
66        },
67        {
68            text => 'Atom',
69            type => 'application/atom+xml',
70            path => 'feed',
71            query => {
[52]72                %$query,
[38]73            },
74        },
75        {
[49]76            text => 'CSV',
77            type => 'text/csv',
78            query => {
[52]79                %$query,
[49]80                format => 'csv',
81            },
82        },
83        {
[38]84            text => 'URI List',
85            type => 'text/uri-list',
86            query => {
[52]87                %$query,
[40]88                format => 'text',
[38]89            },
[49]90        },
[50]91        {
92            text => 'HTML',
93            type => 'text/html',
94            query => {
[52]95                %$query,
[50]96            },
97        },
[38]98    );
99
100    for my $link (@links) {
101        $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate';
[59]102        $link->{href} = URI->new_abs($link->{path} || '', $self->base_uri);
[38]103        $link->{href}->query_form($link->{query});
104    }
105
106    return @links;
107}
108
[59]109sub find_or_new {
[5]110    my $self = shift;
111
[59]112    my $bookmark = $self->bookmarks->get_bookmark({ uri => $self->request->param('uri') });
113    if ($bookmark) {
114        # redirect to the view of the existing bookmark
115        return [301, [Location => $bookmark->bookmark_uri], []];
116    } else {
117        # bookmark was not found; show the form to create a new bookmark
118        my $template = Template->new;
119        $template->process(
120            'bookmark.tt',
121            {
122                uri   => $self->request->param('uri'),
123                title => $self->request->param('title') || '',
124            },
125            \my $output,
126        );
127        return [404, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
[5]128    }
[59]129}
[5]130
[59]131sub list {
132    my $self = shift;
133
[5]134    # list all the bookmarks
[59]135    my $mtime = $self->bookmarks->get_last_modified_time;
[57]136
[59]137    my $format = $self->request->param('format') || 'html';
[35]138
[59]139    my @tags = grep { $_ ne '' } $self->request->param('tag');
140    my $query = $self->request->param('q');
141    my $limit = $self->request->param('limit');
142    my $offset = $self->request->param('offset');
143    my @resources = $self->bookmarks->get_bookmarks({
[52]144        query  => $query,
[20]145        tag    => \@tags,
[13]146        limit  => $limit,
147        offset => $offset,
148    });
[59]149    my @all_tags = $self->bookmarks->get_tags({ selected => $tags[0] });
150    my @cotags = $self->bookmarks->get_cotags({
[52]151        query  => $query,
152        tag    => \@tags,
153    });
[31]154   
[52]155    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : '');
[5]156
157    if ($format eq 'json') {
[59]158        my $json = decode_utf8(
[25]159            JSON->new->utf8->convert_blessed->encode({
[7]160                bookmarks => \@resources,
[5]161            })
162        );
[59]163        return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]];
[19]164    } elsif ($format eq 'xbel') {
165        require XML::XBEL;
166        #TODO: conditional support; if XML::XBEL is not present, return a 5xx response
167
168        my $xbel = XML::XBEL->new;
169
170        $xbel->new_document({
[31]171            title => $title,
[19]172        });
173
174        for my $bookmark (@resources) {
[36]175            my $cdatetime = time2isoz $bookmark->ctime;
176            my $mdatetime = time2isoz $bookmark->mtime;
[19]177            # make the timestamps W3C-correct
178            s/ /T/ foreach ($cdatetime, $mdatetime);
179
180            $xbel->add_bookmark({
[36]181                href     => $bookmark->uri,
182                title    => $bookmark->title,
183                desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
[19]184                added    => $cdatetime,
185                #XXX: are we sure that modified is the mtime of the bookmark or the resource?
186                modified => $mdatetime,
187            });
188        }
189
[59]190        return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]];
[36]191    } elsif ($format eq 'text') {
[59]192        my $text = join '', 
[36]193            map {
194                sprintf "# %s\n# Tags: %s\n%s\n",
195                $_->title,
196                join(', ', @{ $_->tags }), 
197                $_->uri
198            } @resources;
[59]199        return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]];
[49]200    } elsif ($format eq 'csv') {
[51]201        require Text::CSV::Encoded;
[59]202        my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' });
[49]203        my $text = qq{id,uri,title,tags,ctime,mtime\n};
204        for my $bookmark (@resources) {
205            my $success = $csv->combine(
206                $bookmark->id,
207                $bookmark->uri,
208                $bookmark->title,
209                join(' ', @{ $bookmark->tags }),
210                $bookmark->ctime,
211                $bookmark->mtime,
212            );
213            $text .= $csv->string . "\n" if $success;
214        }
215
216        # include the local timestamp in the attchment filename
217        my $dt = time2iso;
218        $dt =~ s/[^\d]//g;
219
[59]220        my $filename = sprintf(
221            'bookmarks-%s-%s.csv',
222            join('_', @tags),
223            $dt,
[49]224        );
[59]225
226        return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]];
[5]227    } else {
[59]228        my $template = Template->new;
[5]229
[59]230        $template->process(
[5]231            'list.tt',
232            {
[59]233                base_url     => $self->base_uri,
[31]234                title        => $title,
[52]235                query        => $query,
[35]236                selected_tag => $tags[0],
[20]237                search_tags  => \@tags,
[52]238                links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ],
[20]239                all_tags     => \@all_tags,
[5]240                cotags       => \@cotags,
241                resources    => \@resources,
242            },
[59]243            \my $output,
[5]244        );
[59]245        return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
[5]246    }
247}
248
[9]249sub feed {
250    my $self = shift;
251
[59]252    my $query = $self->request->param('q');
253    my @tags = grep { $_ ne '' } $self->request->param('tag');
[31]254    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '');
255
[33]256    require XML::Atom;
257    $XML::Atom::DefaultVersion = "1.0";
258
[9]259    require XML::Atom::Feed;
260    require XML::Atom::Entry;
261    require XML::Atom::Link;
[39]262    require XML::Atom::Category;
[9]263
264    my $feed = XML::Atom::Feed->new;
[31]265    $feed->title($title);
[32]266
[52]267    for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) {
[38]268        my $atom_link = XML::Atom::Link->new;
269        $atom_link->type($link->{type});
270        $atom_link->rel($link->{rel});
271        $atom_link->href($link->{href}->canonical);
272        $feed->add_link($atom_link);
273    }
[32]274
[9]275    # construct a feed from the most recent 12 bookmarks
[59]276    for my $bookmark ($self->bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) {
[9]277        my $entry = XML::Atom::Entry->new;
[31]278        $entry->id($bookmark->bookmark_uri->canonical);
279        $entry->title($bookmark->title);
[39]280       
[9]281        my $link = XML::Atom::Link->new;
[31]282        $link->href($bookmark->uri);
[9]283        $entry->add_link($link);
[39]284       
[31]285        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));
[39]286
287        my $cdatetime = time2isoz $bookmark->ctime;
[33]288        my $mdatetime = time2isoz $bookmark->mtime;
289        # make the timestamp W3C-correct
[39]290        s/ /T/ foreach ($cdatetime, $mdatetime);
291        $entry->published($cdatetime);
[33]292        $entry->updated($mdatetime);
[39]293       
294        for my $tag (@{ $bookmark->tags }) {
295            my $category = XML::Atom::Category->new;
296            $category->term($tag);
297            $entry->add_category($category);
298        }
299
[9]300        $feed->add_entry($entry);
301    }
302
[59]303    return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]];
[9]304}
305
[62]306#TODO: better method name
307# returns 1 if there is an If-Modified-Since header and it is newer than the given $mtime
308# returns 0 if there is an If-Modified-Since header but the $mtime is newer
309# returns undef if there is no If-Modified-Since header
310sub _check_modified {
311    my $self = shift;
312    my $mtime = shift;
313
314    # check If-Modified-Since header to return cache response
315    if ($self->request->env->{HTTP_IF_MODIFIED_SINCE}) {
316        my $cache_time = str2time($self->request->env->{HTTP_IF_MODIFIED_SINCE});
317        return $mtime <= $cache_time ? 1 : 0;
318    } else {
319        return;
320    }
321}
322
[5]323sub view {
[59]324    my ($self, $id) = @_;
[5]325
[59]326    my $format = $self->request->param('format') || 'html';
327
328    my $bookmark = $self->get_bookmark({ id => $id });
[5]329    if ($bookmark) {
[62]330        return [304, [], []] if $self->_check_modified($bookmark->mtime);
331
[59]332        my $last_modified = time2str($bookmark->mtime);
[57]333       
[30]334        if ($format eq 'json') {
[59]335            my $json = decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark));
336            return [200, ['Content-Type' => 'application/json; charset=UTF-8', 'Last-Modified' => $last_modified], [$json]];
[5]337        } else {
[30]338            # display the bookmark form for this bookmark
339            $bookmark->{exists} = 1;
340            $bookmark->{created} = "Created " . localtime($bookmark->ctime);
341            $bookmark->{created} .= '; Updated ' . localtime($bookmark->mtime) unless $bookmark->ctime == $bookmark->mtime;
[59]342            my $template = Template->new;
343            $template->process(
[30]344                'bookmark.tt',
345                $bookmark,
[59]346                \my $output,
[30]347            );
[59]348            return [200, ['Content-Type' => 'text/html; charset=UTF-8', 'Last-Modified' => $last_modified], [$output]];
[30]349        }
350    } else {
[59]351        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
[30]352    }
353}
354
355sub view_field {
[59]356    my ($self, $id, $field) = @_;
[30]357
[62]358    my $bookmark = $self->get_bookmark({ id => $id });
[30]359    if ($bookmark) {
[62]360        return [304, [], []] if $self->_check_modified($bookmark->mtime);
361
[63]362        # check whether the requested field is part of the bookmark
363        if (!$bookmark->meta->has_attribute($field)) {
364            return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$field" is not a valid bookmark data field}]];
[5]365        }
[63]366
367        # respond with just the requested field as plain text
368        my $value = $bookmark->$field;
[62]369        my $last_modified = time2str($bookmark->mtime);
370        return [200, ['Content-Type' => 'text/plain; charset=UTF-8', 'Last-Modified' => $last_modified], [ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value]];
[5]371    } else {
[59]372        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
[5]373    }
374}
375
[63]376sub create_and_redirect {
[5]377    my $self = shift;
[59]378
379    my $uri   = $self->request->param('uri');
380    my $title = $self->request->param('title');
381    my @tags  = split ' ', $self->request->param('tags');
382
383    my $bookmark = $self->bookmarks->add({
[5]384        uri   => $uri,
385        title => $title,
386        tags  => \@tags,
387    });
388
[59]389    #TODO: not RESTful; the proper RESTful response would be a 201
390    return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
[5]391}
392
[63]393sub update_and_redirect {
[45]394    my $self = shift;
[59]395    my $id = shift;
[45]396
[59]397    my $bookmark = $self->bookmarks->get_bookmark({ id => $id });
[45]398    if ($bookmark) {
399        # update the URI, title, and tags
[59]400        $bookmark->uri($self->request->param('uri'));
401        $bookmark->title($self->request->param('title'));
402        $bookmark->tags([ split ' ', $self->request->param('tags') || '' ]);
[45]403
[46]404        # write to the database
[59]405        $self->bookmarks->update($bookmark);
[46]406
[59]407        #TODO: not RESTful; proper response would be a 200
408        return [303, ['Location' => $bookmark->bookmark_uri->canonical], []];
[45]409    } else {
[59]410        return [404, ['Content-Type' => 'text/plain; charset=UTF-8'], ["Boomark $id not found"]];
[45]411    }
412}
413
[5]4141;
Note: See TracBrowser for help on using the repository browser.