source: bookmarks/trunk/BookmarkController.pm @ 66

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