package Bookmarks::List; use Moose; use Encode; use HTTP::Date qw{time2iso time2isoz}; has bookmarks => ( is => 'ro', isa => 'Bookmarks', ); has search => ( is => 'ro', isa => 'Bookmarks::Search', handles => [qw{query tags limit offset results}], ); has title => ( is => 'ro', builder => '_build_title', lazy => 1, ); sub _get_list_links { my $self = shift; my ($self_type, $query) = @_; # remove extraneous blank ?q= parameters delete $query->{q} if defined $query->{q} && $query->{q} eq ''; my @links = ( { text => 'JSON', type => 'application/json', query => { %$query, format => 'json', }, }, { text => 'XBEL', type => 'application/xml', query => { %$query, format => 'xbel', }, }, { text => 'Atom', type => 'application/atom+xml', path => 'feed', query => { %$query, }, }, { text => 'CSV', type => 'text/csv', query => { %$query, format => 'csv', }, }, { text => 'URI List', type => 'text/uri-list', query => { %$query, format => 'text', }, }, { text => 'HTML', type => 'text/html', query => { %$query, }, }, ); for my $link (@links) { $link->{rel} = $link->{type} eq $self_type ? 'self' : 'alternate'; $link->{href} = URI->new_abs($link->{path} || '', $self->bookmarks->base_uri); $link->{href}->query_form($link->{query}); } return @links; } sub _build_title { my $self = shift; return 'Bookmarks' . (@{ $self->tags } ? " tagged as " . join(' & ', @{ $self->tags }) : '') . ($self->query ? " matching '" . $self->query . "'" : ''); } sub as_json { my $self = shift; require JSON; my $json = decode_utf8( JSON->new->utf8->convert_blessed->encode({ bookmarks => [ map { $_->to_hashref } @{ $self->results } ], }) ); return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]]; } sub as_xbel { my $self = shift; require XML::XBEL; #TODO: conditional support; if XML::XBEL is not present, return a 5xx response my $xbel = XML::XBEL->new; $xbel->new_document({ title => $self->title, }); for my $bookmark (@{ $self->results }) { my $cdatetime = time2isoz $bookmark->ctime; my $mdatetime = time2isoz $bookmark->mtime; # make the timestamps W3C-correct s/ /T/ foreach ($cdatetime, $mdatetime); $xbel->add_bookmark({ href => $bookmark->uri, title => $bookmark->title, desc => 'Tags: ' . join(', ', @{ $bookmark->tags }), added => $cdatetime, #XXX: are we sure that modified is the mtime of the bookmark or the resource? modified => $mdatetime, }); } return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]]; } sub as_text { my $self = shift; my $text = join '', map { sprintf "# %s\n# Tags: %s\n%s\n", $_->title, join(', ', @{ $_->tags }), $_->uri } @{ $self->results }; return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]]; } sub as_csv { my $self = shift; require Text::CSV::Encoded; my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' }); my $text = qq{id,uri,title,tags,ctime,mtime\n}; for my $bookmark (@{ $self->results }) { my $success = $csv->combine( $bookmark->id, $bookmark->uri, $bookmark->title, join(' ', @{ $bookmark->tags }), $bookmark->ctime, $bookmark->mtime, ); $text .= $csv->string . "\n" if $success; } # include the local timestamp in the attchment filename my $dt = time2iso; $dt =~ s/[^\d]//g; my $filename = sprintf( 'bookmarks-%s-%s.csv', join('_', @{ $self->tags }), $dt, ); return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]]; } sub as_html { my $self = shift; require Template; require File::Basename; my $template = Template->new({ INCLUDE_PATH => File::Basename::dirname($INC{'Bookmarks/List.pm'}) }); my @all_tags = $self->bookmarks->get_tags({ selected => @{ $self->tags }[0] }); my @cotags = $self->bookmarks->get_cotags({ search => $self->search }); $template->process( 'list.tt', { base_url => $self->bookmarks->base_uri, title => $self->title, query => $self->query, selected_tag => @{ $self->tags }[0], search_tags => $self->tags, links => [ $self->_get_list_links('text/html', { q => $self->query, tag => $self->tags }) ], all_tags => \@all_tags, cotags => \@cotags, resources => $self->results, }, \my $output, ); return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; } sub as_atom { my $self = shift; require XML::Atom; $XML::Atom::DefaultVersion = "1.0"; require XML::Atom::Feed; require XML::Atom::Entry; require XML::Atom::Link; require XML::Atom::Category; my $feed = XML::Atom::Feed->new; $feed->title($self->title); for my $link ($self->_get_list_links('application/atom+xml', { q => $self->query, tag => $self->tags })) { my $atom_link = XML::Atom::Link->new; $atom_link->type($link->{type}); $atom_link->rel($link->{rel}); $atom_link->href($link->{href}->canonical); $feed->add_link($atom_link); } for my $bookmark (@{ $self->results }) { my $entry = XML::Atom::Entry->new; $entry->id($bookmark->bookmark_uri->canonical); $entry->title($bookmark->title); my $link = XML::Atom::Link->new; $link->href($bookmark->uri); $entry->add_link($link); $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags })); my $cdatetime = time2isoz $bookmark->ctime; my $mdatetime = time2isoz $bookmark->mtime; # make the timestamp W3C-correct s/ /T/ foreach ($cdatetime, $mdatetime); $entry->published($cdatetime); $entry->updated($mdatetime); for my $tag (@{ $bookmark->tags }) { my $category = XML::Atom::Category->new; $category->term($tag); $entry->add_category($category); } $feed->add_entry($entry); } return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]]; } 1;