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
Line 
1package BookmarkApp;
2use strict;
3use warnings;
4use base qw{CGI::Application};
5
6use CGI::Application::Plugin::TT;
7
8use Encode;
9use HTTP::Date qw{time2isoz time2iso time2str str2time};
10use JSON;
11use Bookmarks;
12use URI;
13
14sub setup {
15    my $self = shift;
16    $self->mode_param(path_info => 1);
17    $self->run_modes([qw{
18        list
19        feed
20        view
21        view_field
22        create
23        edit
24    }]);
25
26    my $url = $self->query->url;
27    $url .= '/' unless $url =~ m{/$};
28    my $base_uri = URI->new($url);
29
30    my $bookmarks = Bookmarks->new({
31        dbname   => $self->param('dbname'),
32        base_uri => $base_uri,
33    });
34
35    $self->param(
36        base_uri  => $base_uri,
37        bookmarks => $bookmarks,
38    );
39}
40
41sub _bookmarks { $_[0]->param('bookmarks') }
42
43sub _get_list_links {
44    my $self = shift;
45    my ($self_type, $query) = @_;
46    my @links = (
47        {
48            text => 'JSON',
49            type => 'application/json',
50            query => {
51                %$query,
52                format => 'json',
53            },
54        },
55        {
56            text => 'XBEL',
57            type => 'application/xml',
58            query => {
59                %$query,
60                format => 'xbel',
61            },
62        },
63        {
64            text => 'Atom',
65            type => 'application/atom+xml',
66            path => 'feed',
67            query => {
68                %$query,
69            },
70        },
71        {
72            text => 'CSV',
73            type => 'text/csv',
74            query => {
75                %$query,
76                format => 'csv',
77            },
78        },
79        {
80            text => 'URI List',
81            type => 'text/uri-list',
82            query => {
83                %$query,
84                format => 'text',
85            },
86        },
87        {
88            text => 'HTML',
89            type => 'text/html',
90            query => {
91                %$query,
92            },
93        },
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
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'))) {
112        my $bookmark = $self->_bookmarks->get_bookmark({ uri => $uri });
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            );
129            return $self->tt_process(
130                'bookmark.tt',
131                $bookmark,
132            );
133        }
134    }
135
136    # list all the bookmarks
137    my $mtime = $self->_bookmarks->get_last_modified_time;
138
139    my $format = $q->param('format') || 'html';
140
141    my @tags = grep { $_ ne '' } $q->param('tag');
142    my $query = $q->param('q');
143    my $limit = $q->param('limit');
144    my $offset = $q->param('offset');
145    my @resources = $self->_bookmarks->get_bookmarks({
146        query  => $query,
147        tag    => \@tags,
148        limit  => $limit,
149        offset => $offset,
150    });
151    my @all_tags = $self->_bookmarks->get_tags({ selected => $tags[0] });
152    my @cotags = $self->_bookmarks->get_cotags({
153        query  => $query,
154        tag    => \@tags,
155    });
156   
157    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : '');
158
159    if ($format eq 'json') {
160        $self->header_props(
161            -type    => 'application/json',
162            -charset => 'UTF-8',
163        );
164        return decode_utf8(
165            JSON->new->utf8->convert_blessed->encode({
166                bookmarks => \@resources,
167            })
168        );
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({
176            title => $title,
177        });
178
179        for my $bookmark (@resources) {
180            my $cdatetime = time2isoz $bookmark->ctime;
181            my $mdatetime = time2isoz $bookmark->mtime;
182            # make the timestamps W3C-correct
183            s/ /T/ foreach ($cdatetime, $mdatetime);
184
185            $xbel->add_bookmark({
186                href     => $bookmark->uri,
187                title    => $bookmark->title,
188                desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
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;
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;
213    } elsif ($format eq 'csv') {
214        require Text::CSV::Encoded;
215        my $csv = Text::CSV::Encoded->new({ encoding => 'utf8' });
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;
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
249        return $self->tt_process(
250            'list.tt',
251            {
252                base_url     => $base_url,
253                title        => $title,
254                query        => $query,
255                selected_tag => $tags[0],
256                search_tags  => \@tags,
257                links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ],
258                all_tags     => \@all_tags,
259                cotags       => \@cotags,
260                resources    => \@resources,
261            },
262        );
263    }
264}
265
266sub feed {
267    my $self = shift;
268    my $q = $self->query;
269
270    my $query = $q->param('q');
271    my @tags = grep { $_ ne '' } $q->param('tag');
272    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '');
273
274    require XML::Atom;
275    $XML::Atom::DefaultVersion = "1.0";
276
277    require XML::Atom::Feed;
278    require XML::Atom::Entry;
279    require XML::Atom::Link;
280    require XML::Atom::Category;
281
282    my $feed = XML::Atom::Feed->new;
283    $feed->title($title);
284
285    my $feed_uri = URI->new_abs('feed', $self->param('base_uri'));
286    $feed_uri->query_form(tag => \@tags);
287    $feed->id($feed_uri->canonical);
288
289    for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) {
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    }
296
297    # construct a feed from the most recent 12 bookmarks
298    for my $bookmark ($self->_bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) {
299        my $entry = XML::Atom::Entry->new;
300        $entry->id($bookmark->bookmark_uri->canonical);
301        $entry->title($bookmark->title);
302       
303        my $link = XML::Atom::Link->new;
304        $link->href($bookmark->uri);
305        $entry->add_link($link);
306       
307        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));
308
309        my $cdatetime = time2isoz $bookmark->ctime;
310        my $mdatetime = time2isoz $bookmark->mtime;
311        # make the timestamp W3C-correct
312        s/ /T/ foreach ($cdatetime, $mdatetime);
313        $entry->published($cdatetime);
314        $entry->updated($mdatetime);
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
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
332sub view {
333    my $self = shift;
334    my $id = $self->param('id');
335    my $format = $self->query->param('format') || 'html';
336
337    my $bookmark = $self->_bookmarks->get_bookmark({ id => $id });
338    if ($bookmark) {
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       
351        if ($format eq 'json') {
352            $self->header_props(
353                -type    => 'application/json',
354                -charset => 'UTF-8',
355            );
356            return decode_utf8(JSON->new->utf8->convert_blessed->encode($bookmark));
357        } else {
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',
365                -Last_Modified => time2str($bookmark->mtime),
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
387    my $bookmark = $self->_bookmarks->get_bookmark({ id => $id });
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/) {
393                $self->header_props(
394                    -type    => 'text/plain',
395                    -charset => 'UTF-8',
396                    -status  => 404,
397                );
398                return qq{"$field" is not a valid bookmark data field};
399            } else {
400                die $@;
401            }
402        }
403        $self->header_props(
404            -type    => 'text/plain',
405            -charset => 'UTF-8',
406        );
407        return ref $value eq 'ARRAY' ? join(' ', @{ $value }) : $value;
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
418sub create {
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');
424    my $bookmark = $self->_bookmarks->add({
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(
441        -uri => $bookmark->bookmark_uri->canonical,
442        -status => 303,
443    );
444}
445
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'));
456        $bookmark->tags([ split ' ', $q->param('tags') || '' ]);
457
458        # write to the database
459        $self->_bookmarks->update($bookmark);
460
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
4771;
Note: See TracBrowser for help on using the repository browser.