source: bookmarks/trunk/BookmarkController.pm @ 59

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