File Coverage

blib/lib/HTTP/Link/Parser.pm
Criterion Covered Total %
statement 87 146 59.5
branch 21 50 42.0
condition 1 18 5.5
subroutine 14 20 70.0
pod 5 5 100.0
total 128 239 53.5


line stmt bran cond sub pod time code
1             package HTTP::Link::Parser;
2              
3 1     1   80488 use 5.010;
  1         6  
  1         67  
4 1     1   6 use strict;
  1         2  
  1         42  
5 1     1   5 no warnings;
  1         7  
  1         230  
6              
7             BEGIN
8             {
9 1     1   3 $HTTP::Link::Parser::AUTHORITY = 'cpan:TOBYINK';
10 1         1 $HTTP::Link::Parser::VERSION = '0.200';
11            
12 1         6 require Exporter;
13 1         20 our @ISA = qw(Exporter);
14 1         5 our %EXPORT_TAGS = (
15             'all' => [qw/parse_links_into_model parse_links_to_rdfjson parse_links_to_list parse_single_link relationship_uri/],
16             'standard' => [qw/parse_links_into_model parse_links_to_rdfjson/],
17             );
18 1         2 our @EXPORT_OK = @{ $EXPORT_TAGS{'all'} };
  1         3  
19 1         2 our @EXPORT = @{ $EXPORT_TAGS{'standard'} };
  1         33  
20             }
21              
22 1     1   6 use Carp qw(croak carp);
  1         1  
  1         92  
23 1     1   1076 use Encode qw(decode encode_utf8);
  1         73999  
  1         116  
24 1     1   12 use Scalar::Util qw(blessed);
  1         2  
  1         116  
25 1     1   38627 use URI;
  1         14250  
  1         34  
26 1     1   10 use URI::Escape;
  1         2  
  1         74  
27              
28             use constant (
29 1         2156 LINK_NAMESPACE => 'http://www.iana.org/assignments/relation/',
30 1     1   5 );
  1         1  
31              
32             sub parse_links_into_model
33             {
34 0     0 1 0 my ($response, $model) = @_;
35            
36 0 0 0     0 croak "Parameter to parse_links_into_model should be an HTTP::Message"
37             unless blessed($response) && $response->isa('HTTP::Message');
38            
39 0         0 require RDF::Trine;
40            
41 0   0     0 my $model ||= RDF::Trine::Model->temporary_model;
42 0         0 $model->add_hashref(parse_links_to_rdfjson($response));
43 0         0 return $model;
44             }
45              
46             sub parse_links_to_rdfjson
47             {
48 0     0 1 0 my ($response) = @_;
49            
50 0 0 0     0 croak "Parameter to parse_links_to_rdfjson should be an HTTP::Message."
51             unless blessed($response) && $response->isa('HTTP::Message');
52            
53 0         0 my $base = URI->new($response->base);
54 0         0 my $links = parse_links_to_list($response);
55 0         0 my $rv = {};
56            
57 0         0 foreach my $link (@$links)
58             {
59 0         0 my $subject = $base;
60            
61 0 0       0 $subject = $link->{'anchor'}
62             if defined $link->{'anchor'};
63            
64 0         0 my $object = $link->{'URI'};
65            
66 0         0 foreach my $r (@{ $link->{'rel'} })
  0         0  
67             {
68 0         0 my $r1 = relationship_uri($r);
69 0         0 push @{ $rv->{ $subject }->{ $r1 } },
  0         0  
70             {
71             'value' => "$object",
72             'type' => 'uri',
73             };
74             }
75            
76 0         0 foreach my $r (@{ $link->{'rev'} })
  0         0  
77             {
78 0         0 my $r1 = relationship_uri($r);
79 0         0 push @{ $rv->{ $object }->{ $r1 } },
  0         0  
80             {
81             'value' => "$subject",
82             'type' => 'uri',
83             };
84             }
85            
86 0 0       0 if (defined $link->{'title'})
87             {
88 0 0 0     0 if (blessed($link->{'title'}) && $link->{'title'}->isa('HTTP::Link::Parser::PlainLiteral'))
89             {
90 0         0 push @{ $rv->{ $object }->{ 'http://purl.org/dc/terms/title' } },
  0         0  
91             {
92             'value' => encode_utf8($link->{'title'}.''),
93             'type' => 'literal',
94             'lang' => $link->{'title'}->lang,
95             };
96             }
97             else
98             {
99 0         0 push @{ $rv->{ $object }->{ 'http://purl.org/dc/terms/title' } },
  0         0  
100             {
101             'value' => $link->{'title'},
102             'type' => 'literal',
103             };
104             }
105             }
106            
107 0 0       0 if (defined $link->{'title*'})
108             {
109 0         0 foreach my $t (@{ $link->{'title*'} })
  0         0  
110             {
111 0         0 push @{ $rv->{ $object }->{ 'http://purl.org/dc/terms/title' } },
  0         0  
112             {
113             'value' => encode_utf8("$t"),
114             'type' => 'literal',
115             'lang' => $t->lang,
116             };
117             }
118             }
119            
120 0 0       0 if (defined $link->{'hreflang'})
121             {
122 0         0 foreach my $lang (@{ $link->{'hreflang'} })
  0         0  
123             {
124 0         0 push @{ $rv->{ $object }->{ 'http://purl.org/dc/terms/language' } },
  0         0  
125             {
126             'value' => 'http://www.lingvoj.org/lingvo/' . uri_escape(lc $lang),
127             'type' => 'uri',
128             };
129             }
130             }
131            
132 0 0 0     0 if (defined $link->{'type'} && $link->{'type'} =~ m?([A-Z0-9\!\#\$\&\.\+\-\^\_]{1,127})/([A-Z0-9\!\#\$\&\.\+\-\^\_]{1,127})?i)
133             {
134 0         0 my $type = lc $1;
135 0         0 my $subtype = lc $2;
136 0         0 push @{ $rv->{ $object }->{ 'http://purl.org/dc/terms/format' } },
  0         0  
137             {
138             'value' => 'http://www.iana.org/assignments/media-types/'.uri_escape($type).'/'.uri_escape($subtype),
139             'type' => 'uri',
140             };
141             }
142             }
143            
144 0         0 return $rv;
145             }
146              
147             sub relationship_uri
148             {
149 0     0 1 0 my ($str) = @_;
150            
151 0 0       0 if ($str =~ /^([a-z][a-z0-9\+\.\-]{0,126})\:/i)
152             {
153             # seems to be an absolute URI, so can safely return "as is".
154 0         0 return $str;
155             }
156            
157 0         0 return LINK_NAMESPACE . lc $str;
158             }
159              
160             sub parse_links_to_list
161             {
162 1     1 1 398 my ($response) = @_;
163            
164 1 50 33     20 croak "Parameter to parse_links_to_list should be an HTTP::Message."
165             unless blessed($response) && $response->isa('HTTP::Message');
166            
167 1         3 my $rv = [];
168 1         6 my $base = URI->new($response->base);
169            
170 1         13127 my $clang;
171 1 50       8 if ($response->header('Content-Language') =~ /^\s*([^,\s]+)/)
172             {
173 0         0 $clang = $1;
174             }
175            
176 1         65 foreach my $header ($response->header('Link'))
177             {
178 6         52 push @$rv, parse_single_link($header, $base, $clang);
179             }
180            
181 1         10 return $rv;
182             }
183              
184             sub parse_single_link
185             {
186 6     6 1 11 my ($hdrv, $base, $default_lang) = @_;
187 6         10 my $rv = {};
188            
189 6         8 my $uri = undef;
190 6 50       25 if ($hdrv =~ /^(\s*<([^>]*)>\s*)/)
191             {
192 6         12 $uri = $2;
193 6         18 $hdrv = substr($hdrv, length($1));
194             }
195             else
196             {
197 0         0 return $rv;
198             }
199            
200 6         17 $rv->{'URI'} = URI->new_abs($uri, $base);
201            
202 6         1766 while ($hdrv =~ /^(\s*\;\s*(\/|[a-z0-9-]+\*?)\s*\=\s*("[^"]*"|[^\s\"\;\,]+)\s*)/i)
203             {
204 16         35 $hdrv = substr($hdrv, length($1));
205 16         24 my $key = lc $2;
206 16         197 my $val = $3;
207            
208 16 100       116 $val =~ s/(^"|"$)//g if ($val =~ /^".*"$/);
209            
210 16 100       62 if ($key eq 'rel')
    100          
    100          
    100          
    100          
    100          
211             {
212 4         23 $val =~ s/(^\s+)|(\s+$)//g;
213 4         9 $val =~ s/\s+/ /g;
214            
215 4         15 my @rels = split / /, $val;
216 4         14 foreach my $rel (@rels)
217 5         7 { push @{ $rv->{'rel'} }, $rel; }
  5         36  
218             }
219             elsif ($key eq 'rev')
220             {
221 2         7 $val =~ s/(^\s+)|(\s+$)//g;
222 2         5 $val =~ s/\s+/ /g;
223            
224 2         7 my @rels = split / /, $val;
225 2         4 foreach my $rel (@rels)
226 2         2 { push @{ $rv->{'rev'} }, $rel; }
  2         19  
227             }
228             elsif ($key eq 'anchor')
229             {
230 1 50       10 $rv->{'anchor'} = URI->new_abs($val, $base)
231             unless defined $rv->{'anchor'};
232             }
233             elsif ($key eq 'title')
234             {
235 6 50       8 if (defined $default_lang)
236             {
237 0         0 my $lit = bless [$val, undef, lc $default_lang], 'HTTP::Link::Parser::PlainLiteral';
238 0         0 push @{ $rv->{'title'} }, $lit;
  0         0  
239             }
240             else
241             {
242 6 50       38 $rv->{'title'} = $val
243             unless defined $rv->{'title'};
244             }
245             }
246             elsif ($key eq 'title*')
247             {
248 1         4 my ($charset, $lang, $string) = split /\'/, $val;
249 1         5 $string = uri_unescape($string);
250 1         29 $string = decode($charset, $string);
251 1         228 my $lit = bless [$string, undef, lc $lang], 'HTTP::Link::Parser::PlainLiteral';
252 1         1 push @{ $rv->{'title*'} }, $lit;
  1         6  
253             }
254             elsif ($key eq 'type')
255             {
256 1 50       41 $rv->{'type'} = $val
257             unless defined $rv->{'type'};
258             }
259             else # hreflang, plus any extended types.
260             {
261 1         2 push @{ $rv->{ $key } }, $val;
  1         5  
262             }
263             }
264            
265 6         210 return $rv;
266             }
267              
268             {
269             package HTTP::Link::Parser::PlainLiteral;
270            
271             use overload
272 2     2   5671 '""' => sub { $_[0]->[0] },
273 1 0   1   10 'eq' => sub { $_[0]->[0] eq $_[1]->[0] and lc $_[0]->[2] eq lc $_[1]->[2] };
  1     0   2  
  1         13  
  0         0  
274            
275 0     0     sub value { $_[0]->[0]; }
276 0 0   0     sub lang { length $_[0]->[2] ? $_[0]->[2] : undef; }
277             }
278              
279             1;
280              
281             __END__
282              
283             =pod
284              
285             =encoding utf-8
286              
287             =for stopwords hreflang prev rel
288              
289             =head1 NAME
290              
291             HTTP::Link::Parser - parse HTTP Link headers
292              
293             =head1 SYNOPSIS
294              
295             use HTTP::Link::Parser ':standard';
296             use LWP::UserAgent;
297            
298             my $ua = LWP::UserAgent->new;
299             my $response = $ua->get("http://example.com/foo");
300            
301             # Parse link headers into an RDF::Trine::Model.
302             my $model = parse_links_into_model($response);
303              
304             # Find data about <http://example.com/foo>.
305             my $iterator = $model->get_statements(
306             RDF::Trine::Node::Resource->new('http://example.com/foo'),
307             undef,
308             undef);
309              
310             while ($statement = $iterator->next)
311             {
312             # Skip data where the value is not a resource (i.e. link)
313             next unless $statement->object->is_resource;
314              
315             printf("Link to <%s> with rel=\"%s\".\n",
316             $statement->object->uri,
317             $statement->predicate->uri);
318             }
319              
320             =head1 DESCRIPTION
321              
322             HTTP::Link::Parser parses HTTP "Link" headers found in an
323             HTTP::Response object. Headers should conform to the format
324             described in RFC 5988.
325              
326             =head2 Functions
327              
328             To export all functions:
329              
330             use HTTP::Link::Parser ':all';
331              
332             =over 4
333              
334             =item C<< parse_links_into_model($response, [$existing_model]) >>
335              
336             Takes an L<HTTP::Response> object (or in fact, any L<HTTP::Message> object)
337             and returns an L<RDF::Trine::Model> containing link data extracted from the
338             response. Dublin Core is used to encode 'hreflang', 'title' and 'type' link
339             parameters.
340              
341             C<$existing_model> is an RDF::Trine::Model to add data to. If omitted, a
342             new, empty model is created.
343              
344             =item C<< parse_links_to_rdfjson($response) >>
345              
346             Returns a hashref with a structure inspired by the RDF/JSON
347             specification. This can be thought of as a shortcut for:
348              
349             parse_links_into_model($response)->as_hashref
350              
351             But it's faster as no intermediate model is built.
352              
353             =item C<< relationship_uri($short) >>
354              
355             This function is not exported by default.
356              
357             It may be used to convert short strings identifying relationships,
358             such as "next" and "prev", into longer URIs identifying the same
359             relationships, such as "http://www.iana.org/assignments/relation/next"
360             and "http://www.iana.org/assignments/relation/prev".
361              
362             If passed a string which is a URI already, simply returns it as-is.
363              
364             =back
365              
366             =head2 Internal Functions
367              
368             These are really just internal implementations, but you can use them if you
369             like.
370              
371             =over
372              
373             =item C<< parse_links_to_list($response) >>
374              
375             This function is not exported by default.
376              
377             Returns an arrayref of hashrefs. Each hashref contains keys
378             corresponding to the link parameters of the link, and a key called
379             'URI' corresponding to the target of the link.
380              
381             The 'rel' and 'rev' keys are arrayrefs containing lists of
382             relationships. If the Link used the short form of a registered
383             relationship, then the short form is present on this list. Short
384             forms can be converted to long forms (URIs) using the
385             C<relationship_uri> function.
386              
387             The structure returned by this function should not be considered
388             stable.
389              
390             =item C<< parse_single_link($link, $base, [$default_lang]) >>
391              
392             This function is not exported by default.
393              
394             This parses a single Link header (minus the "Link:" bit itself) into a hashref
395             structure. A base URI must be included in case the link contains relative URIs.
396             A default language can be provided for the 'title' parameter.
397              
398             The structure returned by this function should not be considered stable.
399              
400             =back
401              
402             =head1 BUGS
403              
404             Please report any bugs to L<http://rt.cpan.org/>.
405              
406             =head1 SEE ALSO
407              
408             L<http://www.ietf.org/rfc/rfc5988.txt>.
409              
410             L<RDF::Trine>,
411             L<HTTP::Response>,
412             L<XRD::Parser>,
413             L<HTTP::LRDD>.
414              
415             L<http://n2.talis.com/wiki/RDF_JSON_Specification>.
416              
417             L<http://www.perlrdf.org/>.
418              
419             =head1 AUTHOR
420              
421             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
422              
423             =head1 COPYRIGHT AND LICENCE
424              
425             Copyright (C) 2009-2011, 2014 by Toby Inkster
426              
427             Permission is hereby granted, free of charge, to any person obtaining a copy
428             of this software and associated documentation files (the "Software"), to deal
429             in the Software without restriction, including without limitation the rights
430             to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
431             copies of the Software, and to permit persons to whom the Software is
432             furnished to do so, subject to the following conditions:
433              
434             The above copyright notice and this permission notice shall be included in
435             all copies or substantial portions of the Software.
436              
437             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
438             IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
439             FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
440             AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
441             LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
442             OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
443             THE SOFTWARE.
444              
445             =cut
446