source: bookmarks/trunk/BookmarkApp.pm @ 58

Last change on this file since 58 was 58, checked in by peter, 11 years ago

removed the Last-Modified header from the list resource until we have a better If-Modified-Since algorithm for the list

File size: 13.5 KB
RevLine 
[5]1package BookmarkApp;
2use strict;
3use warnings;
4use base qw{CGI::Application};
5
[6]6use CGI::Application::Plugin::TT;
7
[5]8use Encode;
[57]9use HTTP::Date qw{time2isoz time2iso time2str str2time};
[5]10use JSON;
11use Bookmarks;
[7]12use URI;
13
[5]14sub setup {
15    my $self = shift;
16    $self->mode_param(path_info => 1);
17    $self->run_modes([qw{
18        list
[9]19        feed
[5]20        view
[30]21        view_field
[45]22        create
[5]23        edit
24    }]);
[41]25
[53]26    my $url = $self->query->url;
27    $url .= '/' unless $url =~ m{/$};
28    my $base_uri = URI->new($url);
[37]29
30    my $bookmarks = Bookmarks->new({
[41]31        dbname   => $self->param('dbname'),
[37]32        base_uri => $base_uri,
33    });
34
35    $self->param(
36        base_uri  => $base_uri,
37        bookmarks => $bookmarks,
38    );
[5]39}
40
[37]41sub _bookmarks { $_[0]->param('bookmarks') }
42
[38]43sub _get_list_links {
44    my $self = shift;
[52]45    my ($self_type, $query) = @_;
[38]46    my @links = (
47        {
48            text => 'JSON',
49            type => 'application/json',
50            query => {
[52]51                %$query,
[38]52                format => 'json',
53            },
54        },
55        {
56            text => 'XBEL',
57            type => 'application/xml',
58            query => {
[52]59                %$query,
[38]60                format => 'xbel',
61            },
62        },
63        {
64            text => 'Atom',
65            type => 'application/atom+xml',
66            path => 'feed',
67            query => {
[52]68                %$query,
[38]69            },
70        },
71        {
[49]72            text => 'CSV',
73            type => 'text/csv',
74            query => {
[52]75                %$query,
[49]76                format => 'csv',
77            },
78        },
79        {
[38]80            text => 'URI List',
81            type => 'text/uri-list',
82            query => {
[52]83                %$query,
[40]84                format => 'text',
[38]85            },
[49]86        },
[50]87        {
88            text => 'HTML',
89            type => 'text/html',
90            query => {
[52]91                %$query,
[50]92            },
93        },
[38]94    );
95
96    for my $link (@links) {
97        $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate';
98        $link->{href} = URI->new_abs($link->{path} || '', $self->param('base_uri'));
99        $link->{href}->query_form($link->{query});
100    }
101
102    return @links;
103}
104
[5]105sub list {
106    my $self = shift;
107    my $q = $self->query;
108
109    # check for a uri param, and if there is one present,
110    # see if a bookmark for that URI already exists
111    if (defined(my $uri = $q->param('uri'))) {
[37]112        my $bookmark = $self->_bookmarks->get_bookmark({ uri => $uri });
[5]113        if ($bookmark) {
114            # redirect to the view of the existing bookmark
115            $self->header_type('redirect');
116            $self->header_props(
117                -uri => $q->url . '/' . $bookmark->{id},
118            );
119            return;
120        } else {
121            # bookmark was not found; show the form to create a new bookmark
122            $bookmark->{uri} = $uri;
123            $bookmark->{title} = $q->param('title');
124            $self->header_props(
125                -type    => 'text/html',
126                -charset => 'UTF-8',
127                -status  => 404,
128            );
[6]129            return $self->tt_process(
[5]130                'bookmark.tt',
131                $bookmark,
132            );
133        }
134    }
135
136    # list all the bookmarks
[57]137    my $mtime = $self->_bookmarks->get_last_modified_time;
138
[5]139    my $format = $q->param('format') || 'html';
[35]140
141    my @tags = grep { $_ ne '' } $q->param('tag');
[52]142    my $query = $q->param('q');
[13]143    my $limit = $q->param('limit');
144    my $offset = $q->param('offset');
[37]145    my @resources = $self->_bookmarks->get_bookmarks({
[52]146        query  => $query,
[20]147        tag    => \@tags,
[13]148        limit  => $limit,
149        offset => $offset,
150    });
[37]151    my @all_tags = $self->_bookmarks->get_tags({ selected => $tags[0] });
[52]152    my @cotags = $self->_bookmarks->get_cotags({
153        query  => $query,
154        tag    => \@tags,
155    });
[31]156   
[52]157    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : '');
[5]158
159    if ($format eq 'json') {
160        $self->header_props(
161            -type    => 'application/json',
162            -charset => 'UTF-8',
163        );
164        return decode_utf8(
[25]165            JSON->new->utf8->convert_blessed->encode({
[7]166                bookmarks => \@resources,
[5]167            })
168        );
[19]169    } elsif ($format eq 'xbel') {
170        require XML::XBEL;
171        #TODO: conditional support; if XML::XBEL is not present, return a 5xx response
172
173        my $xbel = XML::XBEL->new;
174
175        $xbel->new_document({
[31]176            title => $title,
[19]177        });
178
179        for my $bookmark (@resources) {
[36]180            my $cdatetime = time2isoz $bookmark->ctime;
181            my $mdatetime = time2isoz $bookmark->mtime;
[19]182            # make the timestamps W3C-correct
183            s/ /T/ foreach ($cdatetime, $mdatetime);
184
185            $xbel->add_bookmark({
[36]186                href     => $bookmark->uri,
187                title    => $bookmark->title,
188                desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
[19]189                added    => $cdatetime,
190                #XXX: are we sure that modified is the mtime of the bookmark or the resource?
191                modified => $mdatetime,
192            });
193        }
194
195        $self->header_props(
196            -type    => 'application/xml',
197            -charset => 'UTF-8',
198        );
199
200        return $xbel->toString;
[36]201    } elsif ($format eq 'text') {
202        $self->header_props(
203            -type    => 'text/uri-list',
204            -charset => 'UTF-8',
205        );
206        return join '', 
207            map {
208                sprintf "# %s\n# Tags: %s\n%s\n",
209                $_->title,
210                join(', ', @{ $_->tags }), 
211                $_->uri
212            } @resources;
[49]213    } elsif ($format eq 'csv') {
[51]214        require Text::CSV::Encoded;
215        my $csv = Text::CSV::Encoded->new({ encoding => 'utf8' });
[49]216        my $text = qq{id,uri,title,tags,ctime,mtime\n};
217        for my $bookmark (@resources) {
218            my $success = $csv->combine(
219                $bookmark->id,
220                $bookmark->uri,
221                $bookmark->title,
222                join(' ', @{ $bookmark->tags }),
223                $bookmark->ctime,
224                $bookmark->mtime,
225            );
226            $text .= $csv->string . "\n" if $success;
227        }
228
229        # include the local timestamp in the attchment filename
230        my $dt = time2iso;
231        $dt =~ s/[^\d]//g;
232
233        $self->header_props(
234            -type       => 'text/csv',
235            -charset    => 'UTF-8',
236            -attachment => 'bookmarks-' . join('_', @tags) . "-$dt.csv",
237        );
238        return $text;
[5]239    } else {
240        $self->header_props(
241            -type    => 'text/html',
242            -charset => 'UTF-8',
243        );
244
245        # set the base URL, adding a trailing slash if needed
246        my $base_url = $q->url;
247        $base_url .= '/' if $base_url =~ m{/bookmarks$};
248
[6]249        return $self->tt_process(
[5]250            'list.tt',
251            {
252                base_url     => $base_url,
[31]253                title        => $title,
[52]254                query        => $query,
[35]255                selected_tag => $tags[0],
[20]256                search_tags  => \@tags,
[52]257                links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ],
[20]258                all_tags     => \@all_tags,
[5]259                cotags       => \@cotags,
260                resources    => \@resources,
261            },
262        );
263    }
264}
265
[9]266sub feed {
267    my $self = shift;
268    my $q = $self->query;
269
[52]270    my $query = $q->param('q');
[35]271    my @tags = grep { $_ ne '' } $q->param('tag');
[31]272    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '');
273
[33]274    require XML::Atom;
275    $XML::Atom::DefaultVersion = "1.0";
276
[9]277    require XML::Atom::Feed;
278    require XML::Atom::Entry;
279    require XML::Atom::Link;
[39]280    require XML::Atom::Category;
[9]281
282    my $feed = XML::Atom::Feed->new;
[31]283    $feed->title($title);
[32]284
[37]285    my $feed_uri = URI->new_abs('feed', $self->param('base_uri'));
[32]286    $feed_uri->query_form(tag => \@tags);
287    $feed->id($feed_uri->canonical);
288
[52]289    for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) {
[38]290        my $atom_link = XML::Atom::Link->new;
291        $atom_link->type($link->{type});
292        $atom_link->rel($link->{rel});
293        $atom_link->href($link->{href}->canonical);
294        $feed->add_link($atom_link);
295    }
[32]296
[9]297    # construct a feed from the most recent 12 bookmarks
[52]298    for my $bookmark ($self->_bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) {
[9]299        my $entry = XML::Atom::Entry->new;
[31]300        $entry->id($bookmark->bookmark_uri->canonical);
301        $entry->title($bookmark->title);
[39]302       
[9]303        my $link = XML::Atom::Link->new;
[31]304        $link->href($bookmark->uri);
[9]305        $entry->add_link($link);
[39]306       
[31]307        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));
[39]308
309        my $cdatetime = time2isoz $bookmark->ctime;
[33]310        my $mdatetime = time2isoz $bookmark->mtime;
311        # make the timestamp W3C-correct
[39]312        s/ /T/ foreach ($cdatetime, $mdatetime);
313        $entry->published($cdatetime);
[33]314        $entry->updated($mdatetime);
[39]315       
316        for my $tag (@{ $bookmark->tags }) {
317            my $category = XML::Atom::Category->new;
318            $category->term($tag);
319            $entry->add_category($category);
320        }
321
[9]322        $feed->add_entry($entry);
323    }
324
325    $self->header_props(
326        -type => 'application/atom+xml',
327        -charset => 'UTF-8',
328    );
329    return $feed->as_xml;
330}
331
[5]332sub view {
333    my $self = shift;
334    my $id = $self->param('id');
[7]335    my $format = $self->query->param('format') || 'html';
[5]336
[37]337    my $bookmark = $self->_bookmarks->get_bookmark({ id => $id });
[5]338    if ($bookmark) {
[57]339        # check If-Modified-Since header to return cache response
340        if ($self->query->env->{HTTP_IF_MODIFIED_SINCE}) {
341            my $cache_time = str2time($self->query->env->{HTTP_IF_MODIFIED_SINCE});
342            if ($bookmark->mtime <= $cache_time) {
343                $self->header_type('redirect');
344                $self->header_props(
345                    -status => 304,
346                );
347                return;
348            }
349        }
350       
[30]351        if ($format eq 'json') {
[5]352            $self->header_props(
[30]353                -type    => 'application/json',
[5]354                -charset => 'UTF-8',
355            );
[30]356            return decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark));
[5]357        } else {
[30]358            # display the bookmark form for this bookmark
359            $bookmark->{exists} = 1;
360            $bookmark->{created} = "Created " . localtime($bookmark->ctime);
361            $bookmark->{created} .= '; Updated ' . localtime($bookmark->mtime) unless $bookmark->ctime == $bookmark->mtime;
362            $self->header_props(
363                -type    => 'text/html',
364                -charset => 'UTF-8',
[57]365                -Last_Modified => time2str($bookmark->mtime),
[30]366            );
367            return $self->tt_process(
368                'bookmark.tt',
369                $bookmark,
370            );
371        }
372    } else {
373        $self->header_props(
374            -type    => 'text/html',
375            -charset => 'UTF-8',
376            -status  => 404,
377        );
378        return "Bookmark $id Not Found";
379    }
380}
381
382sub view_field {
383    my $self = shift;
384    my $id = $self->param('id');
385    my $field = $self->param('field');
386
[37]387    my $bookmark = $self->_bookmarks->get_bookmark({ id => $id });
[30]388    if ($bookmark) {
389        # respond with just the requested field as plain text
390        my $value = eval { $bookmark->$field };
391        if ($@) {
392            if ($@ =~ /Can't locate object method/) {
[7]393                $self->header_props(
[30]394                    -type    => 'text/plain',
[7]395                    -charset => 'UTF-8',
[30]396                    -status  => 404,
[7]397                );
[30]398                return qq{"$field" is not a valid bookmark data field};
[7]399            } else {
[30]400                die $@;
[7]401            }
[5]402        }
[30]403        $self->header_props(
404            -type    => 'text/plain',
405            -charset => 'UTF-8',
406        );
[48]407        return ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value;
[5]408    } else {
409        $self->header_props(
410            -type    => 'text/html',
411            -charset => 'UTF-8',
412            -status  => 404,
413        );
414        return "Bookmark $id Not Found";
415    }
416}
417
[45]418sub create {
[5]419    my $self = shift;
420    my $q = $self->query;
421    my $uri = $q->param('uri');
422    my $title = $q->param('title');
423    my @tags = split ' ', $q->param('tags');
[43]424    my $bookmark = $self->_bookmarks->add({
[5]425        uri   => $uri,
426        title => $title,
427        tags  => \@tags,
428    });
429
430=begin
431
432    my $location = URI->new($q->url);
433    $location->query_form(uri => $uri) if defined $q->url_param('uri');
434    $location->fragment('updated');
435
436=cut
437
438    # return to the form
439    $self->header_type('redirect');
440    $self->header_props(
[43]441        -uri => $bookmark->bookmark_uri->canonical,
[5]442        -status => 303,
443    );
444}
445
[45]446sub edit {
447    my $self = shift;
448    my $q = $self->query;
449    my $id = $self->param('id');
450
451    my $bookmark = $self->_bookmarks->get_bookmark({ id => $id });
452    if ($bookmark) {
453        # update the URI, title, and tags
454        $bookmark->uri($q->param('uri'));
455        $bookmark->title($q->param('title'));
[53]456        $bookmark->tags([ split ' ', $q->param('tags') || '' ]);
[45]457
[46]458        # write to the database
459        $self->_bookmarks->update($bookmark);
460
[45]461        # return to the form
462        $self->header_type('redirect');
463        $self->header_props(
464            -uri => $bookmark->bookmark_uri->canonical,
465            -status => 303,
466        );
467    } else {
468        $self->header_props(
469            -type    => 'text/html',
470            -charset => 'UTF-8',
471            -status  => 404,
472        );
473        return "Bookmark $id Not Found";
474    }
475}
476
[5]4771;
Note: See TracBrowser for help on using the repository browser.