| [70] | 1 | package Bookmarks::List; | 
|---|
| [68] | 2 |  | 
|---|
 | 3 | use Moose; | 
|---|
 | 4 |  | 
|---|
 | 5 | use Encode; | 
|---|
 | 6 | use HTTP::Date qw{time2iso time2isoz}; | 
|---|
 | 7 |  | 
|---|
| [71] | 8 | has bookmarks => ( | 
|---|
| [68] | 9 |     is => 'ro', | 
|---|
| [71] | 10 |     isa => 'Bookmarks', | 
|---|
| [68] | 11 | ); | 
|---|
| [71] | 12 | has search => ( | 
|---|
 | 13 |     is => 'ro', | 
|---|
 | 14 |     isa => 'Bookmarks::Search', | 
|---|
 | 15 |     handles => [qw{query tags limit offset}], | 
|---|
 | 16 | ); | 
|---|
| [68] | 17 | has results => ( is => 'ro' ); | 
|---|
 | 18 | has title => ( | 
|---|
 | 19 |     is => 'ro', | 
|---|
 | 20 |     builder => '_build_title', | 
|---|
 | 21 |     lazy => 1, | 
|---|
 | 22 | ); | 
|---|
 | 23 |  | 
|---|
 | 24 | sub _get_list_links { | 
|---|
 | 25 |     my $self = shift; | 
|---|
 | 26 |     my ($self_type, $query) = @_; | 
|---|
| [69] | 27 |  | 
|---|
 | 28 |     # remove extraneous blank ?q= parameters | 
|---|
 | 29 |     delete $query->{q} if defined $query->{q} && $query->{q} eq ''; | 
|---|
 | 30 |      | 
|---|
| [68] | 31 |     my @links = ( | 
|---|
 | 32 |         { | 
|---|
 | 33 |             text => 'JSON', | 
|---|
 | 34 |             type => 'application/json', | 
|---|
 | 35 |             query => { | 
|---|
 | 36 |                 %$query, | 
|---|
 | 37 |                 format => 'json', | 
|---|
 | 38 |             }, | 
|---|
 | 39 |         }, | 
|---|
 | 40 |         { | 
|---|
 | 41 |             text => 'XBEL', | 
|---|
 | 42 |             type => 'application/xml', | 
|---|
 | 43 |             query => { | 
|---|
 | 44 |                 %$query, | 
|---|
 | 45 |                 format => 'xbel', | 
|---|
 | 46 |             }, | 
|---|
 | 47 |         }, | 
|---|
 | 48 |         { | 
|---|
 | 49 |             text => 'Atom', | 
|---|
 | 50 |             type => 'application/atom+xml', | 
|---|
 | 51 |             path => 'feed', | 
|---|
 | 52 |             query => { | 
|---|
 | 53 |                 %$query, | 
|---|
 | 54 |             }, | 
|---|
 | 55 |         }, | 
|---|
 | 56 |         { | 
|---|
 | 57 |             text => 'CSV', | 
|---|
 | 58 |             type => 'text/csv', | 
|---|
 | 59 |             query => { | 
|---|
 | 60 |                 %$query, | 
|---|
 | 61 |                 format => 'csv', | 
|---|
 | 62 |             }, | 
|---|
 | 63 |         }, | 
|---|
 | 64 |         { | 
|---|
 | 65 |             text => 'URI List', | 
|---|
 | 66 |             type => 'text/uri-list', | 
|---|
 | 67 |             query => { | 
|---|
 | 68 |                 %$query, | 
|---|
 | 69 |                 format => 'text', | 
|---|
 | 70 |             }, | 
|---|
 | 71 |         }, | 
|---|
 | 72 |         { | 
|---|
 | 73 |             text => 'HTML', | 
|---|
 | 74 |             type => 'text/html', | 
|---|
 | 75 |             query => { | 
|---|
 | 76 |                 %$query, | 
|---|
 | 77 |             }, | 
|---|
 | 78 |         }, | 
|---|
 | 79 |     ); | 
|---|
 | 80 |  | 
|---|
 | 81 |     for my $link (@links) { | 
|---|
 | 82 |         $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate'; | 
|---|
 | 83 |         $link->{href} = URI->new_abs($link->{path} || '', $self->bookmarks->base_uri); | 
|---|
 | 84 |         $link->{href}->query_form($link->{query}); | 
|---|
 | 85 |     } | 
|---|
 | 86 |  | 
|---|
 | 87 |     return @links; | 
|---|
 | 88 | } | 
|---|
 | 89 |  | 
|---|
 | 90 | sub _build_title { | 
|---|
 | 91 |     my $self = shift; | 
|---|
 | 92 |     return 'Bookmarks' . (@{ $self->tags } ? " tagged as " . join(' & ', @{ $self->tags }) : '') . ($self->query ? " matching '" . $self->query . "'" : ''); | 
|---|
 | 93 | } | 
|---|
 | 94 |  | 
|---|
 | 95 | sub as_json { | 
|---|
 | 96 |     my $self = shift; | 
|---|
 | 97 |     require JSON; | 
|---|
 | 98 |     my $json = decode_utf8( | 
|---|
 | 99 |         JSON->new->utf8->convert_blessed->encode({ | 
|---|
 | 100 |             bookmarks => $self->results, | 
|---|
 | 101 |         }) | 
|---|
 | 102 |     ); | 
|---|
 | 103 |     return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]]; | 
|---|
 | 104 | } | 
|---|
 | 105 |  | 
|---|
 | 106 | sub as_xbel { | 
|---|
 | 107 |     my $self = shift; | 
|---|
 | 108 |     require XML::XBEL; | 
|---|
 | 109 |     #TODO: conditional support; if XML::XBEL is not present, return a 5xx response | 
|---|
 | 110 |  | 
|---|
 | 111 |     my $xbel = XML::XBEL->new; | 
|---|
 | 112 |  | 
|---|
 | 113 |     $xbel->new_document({ | 
|---|
 | 114 |         title => $self->title, | 
|---|
 | 115 |     }); | 
|---|
 | 116 |  | 
|---|
 | 117 |     for my $bookmark (@{ $self->results }) { | 
|---|
 | 118 |         my $cdatetime = time2isoz $bookmark->ctime; | 
|---|
 | 119 |         my $mdatetime = time2isoz $bookmark->mtime; | 
|---|
 | 120 |         # make the timestamps W3C-correct | 
|---|
 | 121 |         s/ /T/ foreach ($cdatetime, $mdatetime); | 
|---|
 | 122 |  | 
|---|
 | 123 |         $xbel->add_bookmark({ | 
|---|
 | 124 |             href     => $bookmark->uri, | 
|---|
 | 125 |             title    => $bookmark->title, | 
|---|
 | 126 |             desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }), | 
|---|
 | 127 |             added    => $cdatetime, | 
|---|
 | 128 |             #XXX: are we sure that modified is the mtime of the bookmark or the resource? | 
|---|
 | 129 |             modified => $mdatetime, | 
|---|
 | 130 |         }); | 
|---|
 | 131 |     } | 
|---|
 | 132 |  | 
|---|
 | 133 |     return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]]; | 
|---|
 | 134 | } | 
|---|
 | 135 |  | 
|---|
 | 136 | sub as_text { | 
|---|
 | 137 |     my $self = shift; | 
|---|
 | 138 |     my $text = join '',  | 
|---|
 | 139 |         map { | 
|---|
 | 140 |             sprintf "# %s\n# Tags: %s\n%s\n", | 
|---|
 | 141 |             $_->title, | 
|---|
 | 142 |             join(', ', @{ $_->tags }),  | 
|---|
 | 143 |             $_->uri | 
|---|
 | 144 |         } @{ $self->results }; | 
|---|
 | 145 |     return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]]; | 
|---|
 | 146 | } | 
|---|
 | 147 |  | 
|---|
 | 148 | sub as_csv { | 
|---|
 | 149 |     my $self = shift; | 
|---|
 | 150 |     require Text::CSV::Encoded; | 
|---|
 | 151 |     my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' }); | 
|---|
 | 152 |     my $text = qq{id,uri,title,tags,ctime,mtime\n}; | 
|---|
 | 153 |     for my $bookmark (@{ $self->results }) { | 
|---|
 | 154 |         my $success = $csv->combine( | 
|---|
 | 155 |             $bookmark->id, | 
|---|
 | 156 |             $bookmark->uri, | 
|---|
 | 157 |             $bookmark->title, | 
|---|
 | 158 |             join(' ', @{ $bookmark->tags }), | 
|---|
 | 159 |             $bookmark->ctime, | 
|---|
 | 160 |             $bookmark->mtime, | 
|---|
 | 161 |         ); | 
|---|
 | 162 |         $text .= $csv->string . "\n" if $success; | 
|---|
 | 163 |     } | 
|---|
 | 164 |  | 
|---|
 | 165 |     # include the local timestamp in the attchment filename | 
|---|
 | 166 |     my $dt = time2iso; | 
|---|
 | 167 |     $dt =~ s/[^\d]//g; | 
|---|
 | 168 |  | 
|---|
 | 169 |     my $filename = sprintf( | 
|---|
 | 170 |         'bookmarks-%s-%s.csv', | 
|---|
 | 171 |         join('_', @{ $self->tags }), | 
|---|
 | 172 |         $dt, | 
|---|
 | 173 |     ); | 
|---|
 | 174 |  | 
|---|
 | 175 |     return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]]; | 
|---|
 | 176 | } | 
|---|
 | 177 |  | 
|---|
 | 178 | sub as_html { | 
|---|
 | 179 |     my $self = shift; | 
|---|
 | 180 |  | 
|---|
 | 181 |     require Template; | 
|---|
 | 182 |     my $template = Template->new; | 
|---|
 | 183 |  | 
|---|
 | 184 |     my @all_tags = $self->bookmarks->get_tags({ selected => @{ $self->tags }[0] }); | 
|---|
| [71] | 185 |     my @cotags = $self->bookmarks->get_cotags({ search => $self->search }); | 
|---|
| [68] | 186 |  | 
|---|
 | 187 |     $template->process( | 
|---|
 | 188 |         'list.tt', | 
|---|
 | 189 |         { | 
|---|
 | 190 |             base_url     => $self->bookmarks->base_uri, | 
|---|
 | 191 |             title        => $self->title, | 
|---|
 | 192 |             query        => $self->query, | 
|---|
 | 193 |             selected_tag => @{ $self->tags }[0], | 
|---|
 | 194 |             search_tags  => $self->tags, | 
|---|
 | 195 |             links        => [ $self->_get_list_links('text/html', { q => $self->query, tag => $self->tags }) ], | 
|---|
 | 196 |             all_tags     => \@all_tags, | 
|---|
 | 197 |             cotags       => \@cotags, | 
|---|
 | 198 |             resources    => $self->results, | 
|---|
 | 199 |         }, | 
|---|
 | 200 |         \my $output, | 
|---|
 | 201 |     ); | 
|---|
 | 202 |     return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]]; | 
|---|
 | 203 | } | 
|---|
 | 204 |  | 
|---|
 | 205 | sub as_atom { | 
|---|
 | 206 |     my $self = shift; | 
|---|
 | 207 |  | 
|---|
 | 208 |     require XML::Atom; | 
|---|
 | 209 |     $XML::Atom::DefaultVersion = "1.0"; | 
|---|
 | 210 |  | 
|---|
 | 211 |     require XML::Atom::Feed; | 
|---|
 | 212 |     require XML::Atom::Entry; | 
|---|
 | 213 |     require XML::Atom::Link; | 
|---|
 | 214 |     require XML::Atom::Category; | 
|---|
 | 215 |  | 
|---|
 | 216 |     my $feed = XML::Atom::Feed->new; | 
|---|
 | 217 |     $feed->title($self->title); | 
|---|
 | 218 |  | 
|---|
 | 219 |     for my $link ($self->_get_list_links('application/atom+xml', { q => $self->query, tag => $self->tags })) { | 
|---|
 | 220 |         my $atom_link = XML::Atom::Link->new; | 
|---|
 | 221 |         $atom_link->type($link->{type}); | 
|---|
 | 222 |         $atom_link->rel($link->{rel}); | 
|---|
 | 223 |         $atom_link->href($link->{href}->canonical); | 
|---|
 | 224 |         $feed->add_link($atom_link); | 
|---|
 | 225 |     } | 
|---|
 | 226 |  | 
|---|
 | 227 |     for my $bookmark (@{ $self->results }) { | 
|---|
 | 228 |         my $entry = XML::Atom::Entry->new; | 
|---|
 | 229 |         $entry->id($bookmark->bookmark_uri->canonical); | 
|---|
 | 230 |         $entry->title($bookmark->title); | 
|---|
 | 231 |          | 
|---|
 | 232 |         my $link = XML::Atom::Link->new; | 
|---|
 | 233 |         $link->href($bookmark->uri); | 
|---|
 | 234 |         $entry->add_link($link); | 
|---|
 | 235 |          | 
|---|
 | 236 |         $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags })); | 
|---|
 | 237 |  | 
|---|
 | 238 |         my $cdatetime = time2isoz $bookmark->ctime; | 
|---|
 | 239 |         my $mdatetime = time2isoz $bookmark->mtime; | 
|---|
 | 240 |         # make the timestamp W3C-correct | 
|---|
 | 241 |         s/ /T/ foreach ($cdatetime, $mdatetime); | 
|---|
 | 242 |         $entry->published($cdatetime); | 
|---|
 | 243 |         $entry->updated($mdatetime); | 
|---|
 | 244 |          | 
|---|
 | 245 |         for my $tag (@{ $bookmark->tags }) { | 
|---|
 | 246 |             my $category = XML::Atom::Category->new; | 
|---|
 | 247 |             $category->term($tag); | 
|---|
 | 248 |             $entry->add_category($category); | 
|---|
 | 249 |         } | 
|---|
 | 250 |  | 
|---|
 | 251 |         $feed->add_entry($entry); | 
|---|
 | 252 |     } | 
|---|
 | 253 |  | 
|---|
 | 254 |     return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]]; | 
|---|
 | 255 | } | 
|---|
 | 256 |  | 
|---|
 | 257 | 1; | 
|---|