source: bookmarks/trunk/BookmarkApp.pm @ 50

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

ensure the HTML link comes last, so that it shows up as the preferred alternate link for the Atom feed

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