File Coverage

blib/lib/RDF/Endpoint.pm
Criterion Covered Total %
statement 320 484 66.1
branch 65 148 43.9
condition 26 70 37.1
subroutine 34 38 89.4
pod 11 11 100.0
total 456 751 60.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             RDF::Endpoint - A SPARQL Protocol Endpoint implementation
4              
5             =head1 VERSION
6              
7             This document describes RDF::Endpoint version 0.10_01.
8              
9             =head1 SYNOPSIS
10              
11             plackup /usr/local/bin/endpoint.psgi
12              
13             =head1 DESCRIPTION
14              
15             This modules implements the SPARQL Protocol for RDF using the PSGI
16             interface provided by L<Plack>. It may be run with any Plack handler.
17             See L<Plack::Handler> for more details.
18              
19             When this module is used to create a SPARQL endpoint, configuration variables
20             are loaded using L<Config::ZOMG>. An example configuration file rdf_endpoint.json
21             is included with this package. Valid top-level configuration keys include:
22              
23             =over 4
24              
25             =item store
26              
27             This is used to define the underlying L<RDF::Trine::Store> for the
28             endpoint. It can be a hashref of the type that can be passed to
29             L<RDF::Trine::Store>->new_with_config, but a simple string can also be
30             used.
31              
32             =item endpoint
33              
34             A hash of endpoint-specific configuration variables. Valid keys for this hash
35             include:
36              
37             =over 8
38              
39             =item update
40              
41             A boolean value indicating whether Update operations should be allowed to be
42             executed by the endpoint.
43              
44             =item load_data
45              
46             A boolean value indicating whether the endpoint should use URLs that appear in
47             FROM and FROM NAMED clauses to construct a SPARQL dataset by dereferencing the
48             URLs and loading the retrieved RDF content.
49              
50             =item service_description
51              
52             An associative array (hash) containing details on which and how much information
53             to include in the service description provided by the endpoint if no query is
54             included for execution. The boolean values 'default' and 'named_graphs' indicate
55             that the respective SPARQL dataset graphs should be described by the service
56             description.
57              
58             =item html
59              
60             An associative array (hash) containing details on how results should be
61             serialized when the output media type is HTML. The boolean value 'resource_links'
62             specifies whether URI values should be serialized as HTML anchors (links).
63             The boolean value 'embed_images' specifies whether URI values that are typed as
64             foaf:Image should be serialized as HTML images. If 'embed_images' is true, the
65             integer value 'image_width' specifies the image width to be used in the HTML
66             markup (letting the image height scale appropriately).
67              
68             =back
69              
70             =back
71              
72             =head1 EXAMPLE CONFIGURATIONS
73              
74             =head2 Using L<Plack::Handler::Apache2>
75              
76             Using L<Plack::Handler::Apache2>, mod_perl2 can be configured to serve and
77             endpoint using the following configuration:
78              
79             <Location /sparql>
80             SetHandler perl-script
81             PerlResponseHandler Plack::Handler::Apache2
82             PerlSetVar psgi_app /path/to/endpoint.psgi
83             PerlSetEnv RDF_ENDPOINT_CONFIG /path/to/rdf_endpoint.json
84             </Location>
85              
86             To get syntax highlighting and other pretty features, in the
87             VirtualHost section of your server, add three aliases:
88              
89             Alias /js/ /path/to/share/www/js/
90             Alias /favicon.ico /path/to/share/www/favicon.ico
91             Alias /css/ /path/to/share/www/css/
92              
93             The exact location can be determined by finding where the file C<sparql_form.js>.
94              
95             =head1 METHODS
96              
97             =over 4
98              
99             =cut
100              
101             package RDF::Endpoint;
102              
103 4     4   736271 use 5.008;
  4         32  
104 4     4   25 use strict;
  4         9  
  4         107  
105 4     4   21 use warnings;
  4         7  
  4         195  
106             our $VERSION = '0.10_01';
107              
108 4     4   2720 use RDF::Query 2.905;
  4         9344948  
  4         249  
109 4     4   43 use RDF::Trine 0.134 qw(statement iri blank literal);
  4         69  
  4         282  
110              
111 4     4   29 use JSON;
  4         10  
  4         44  
112 4     4   570 use Encode;
  4         11  
  4         348  
113 4     4   30 use File::Spec;
  4         10  
  4         119  
114 4     4   24 use Data::Dumper;
  4         7  
  4         251  
115 4     4   41 use Digest::MD5 qw(md5_base64);
  4         11  
  4         285  
116 4     4   33 use XML::LibXML 1.70;
  4         51  
  4         186  
117 4     4   3653 use Plack::Request;
  4         190687  
  4         215  
118 4     4   2053 use Plack::Response;
  4         8351  
  4         177  
119 4     4   33 use Scalar::Util qw(blessed refaddr);
  4         11  
  4         233  
120 4     4   29 use File::ShareDir qw(dist_dir);
  4         24  
  4         206  
121 4     4   29 use HTTP::Negotiate qw(choose);
  4         13  
  4         179  
122 4     4   28 use RDF::Trine::Namespace qw(rdf xsd);
  4         9  
  4         46  
123 4     4   2789 use RDF::RDFa::Generator 0.200;
  4         620884  
  4         169  
124 4     4   2115 use RDF::TrineX::Compatibility::Attean;
  4         2712  
  4         212  
125 4     4   1312 use IO::Compress::Gzip qw(gzip);
  4         70215  
  4         411  
126 4     4   3092 use HTML::HTML5::Writer qw(DOCTYPE_XHTML_RDFA);
  4         107924  
  4         372  
127 4     4   2414 use Hash::Merge::Simple qw/ merge /;
  4         2117  
  4         318  
128 4     4   35 use Fcntl qw(:flock SEEK_END);
  4         8  
  4         566  
129 4     4   36 use Carp qw(croak);
  4         10  
  4         22390  
130              
131              
132             my $NAMESPACES = {
133             xsd => 'http://www.w3.org/2001/XMLSchema#',
134             'format' => 'http://www.w3.org/ns/formats/',
135             void => 'http://rdfs.org/ns/void#',
136             scovo => 'http://purl.org/NET/scovo#',
137             sd => 'http://www.w3.org/ns/sparql-service-description#',
138             jena => 'java:com.hp.hpl.jena.query.function.library.',
139             arq => 'http://jena.hpl.hp.com/ARQ/function#',
140             ldodds => 'java:com.ldodds.sparql.',
141             fn => 'http://www.w3.org/2005/xpath-functions#',
142             sparql => 'http://www.w3.org/ns/sparql#',
143             vann => 'http://purl.org/vocab/vann/',
144             sde => 'http://kasei.us/ns/service-description-extension#',
145             };
146              
147             =item C<< new ( \%conf ) >>
148              
149             =item C<< new ( $model, \%conf ) >>
150              
151             Returns a new Endpoint object. C<< \%conf >> should be a HASH reference with
152             configuration settings.
153              
154             =cut
155              
156             sub new {
157 2     2 1 584 my $class = shift;
158 2         5 my $arg = shift;
159 2         6 my ($model, $config);
160 2 50 33     33 if (blessed($arg) and $arg->isa('RDF::Trine::Model')) {
161 2         6 $model = $arg;
162 2         4 $config = shift;
163 2         6 delete $config->{store};
164             } else {
165 0         0 $config = $arg;
166 0         0 my $store = RDF::Trine::Store->new( $config->{store} );
167 0 0       0 unless ($store) {
168 0         0 warn "Failed to construct RDF Store object";
169 0         0 return;
170             }
171 0         0 $model = RDF::Trine::Model->new( $store );
172 0 0       0 unless ($model) {
173 0         0 warn "Failed to construct RDF Model object";
174 0         0 return;
175             }
176             }
177            
178 2 50       10 unless ($config->{endpoint}) {
179 0         0 $config->{endpoint} = { %$config };
180             }
181            
182 2 0 33     10 if ($config->{endpoint}{load_data} and $config->{endpoint}{update}) {
183 0         0 die "The load_data and update configuration options cannot be specified together.";
184             }
185            
186 2         15 my $self = bless( {
187             conf => $config,
188             model => $model,
189             start_time => time,
190             }, $class );
191 2         9 $self->service_description(); # pre-generate the service description
192 2         201 return $self;
193             }
194              
195             =item C<< run ( $req ) >>
196              
197             Handles the request specified by the supplied Plack::Request object, returning
198             an appropriate Plack::Response object.
199              
200             =cut
201              
202             sub run {
203 10     10 1 426670 my $self = shift;
204 10         25 my $req = shift;
205            
206 10         27 my $config = $self->{conf};
207 10   50     51 my $endpoint_path = $config->{endpoint}{endpoint_path} || '/sparql';
208 10 100       39 $config->{resource_links} = 1 unless (exists $config->{resource_links});
209 10         28 my $model = $self->{model};
210            
211 10         19 my $content;
212 10         86 my $response = Plack::Response->new;
213              
214 10         150 my $server = "RDF::Endpoint/$VERSION";
215 10 50       37 $server .= " " . $response->headers->header('Server') if defined($response->headers->header('Server'));
216 10         605 $response->headers->header('Server' => $server);
217              
218 10 50       399 unless ($req->path eq $endpoint_path) {
219 0         0 my $path = $req->path_info;
220 0         0 $path =~ s#^/##;
221 0   0     0 my $dir = $ENV{RDF_ENDPOINT_SHAREDIR} || eval { dist_dir('RDF-Endpoint') } || 'share';
222 0         0 my $file = File::Spec->catfile($dir, 'www', $path);
223 0 0       0 if (-r $file) {
224 0 0       0 open( my $fh, '<', $file ) or croak $!;
225 0         0 $response->status(200);
226 0         0 $content = $fh;
227             } else {
228 0         0 my $path = $req->path;
229 0         0 $response->status(404);
230 0         0 $content = <<"END";
231             <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n
232             <h1>Not Found</h1>\n<p>The requested URL $path was not found on this server.</p>\n</body></html>
233             END
234             }
235 0         0 $response->body($content);
236 0         0 return $response;
237             }
238            
239 10         145 my $headers = $req->headers;
240 10   100     2364 my $type = $headers->header('Accept') || 'application/sparql-results+xml';
241 10 100       320 if (my $t = $req->param('media-type')) {
242 1         537 $type = $t;
243 1         6 $headers->header('Accept' => $type);
244             }
245            
246 10   50     3926 my $ae = $req->headers->header('Accept-Encoding') || '';
247            
248 10         381 my $sparql;
249 10         55 my $ct = $req->header('Content-type');
250 10 50 66     351 if ($req->method !~ /^(GET|POST)$/i) {
    50 66        
    50          
    100          
    100          
251 0         0 my $method = uc($req->method);
252 0         0 $content = "Unexpected method $method (expecting GET or POST)";
253 0         0 $self->log_error( $req, $content );
254 0         0 $self->_set_response_error($req, $response, 405, {
255             title => 'Method not allowed',
256             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/bad_http_method',
257             });
258 0         0 $response->header('Allow' => 'GET, POST');
259 0         0 goto CLEANUP;
260             } elsif (defined($ct) and $ct eq 'application/sparql-query') {
261 0         0 $sparql = $req->content;
262             } elsif (defined($ct) and $ct eq 'application/sparql-update') {
263 0 0 0     0 if ($config->{endpoint}{update} and $req->method eq 'POST') {
264 0         0 $sparql = $req->content;
265             }
266             } elsif ($req->param('query')) {
267 5         167 my @sparql = $req->param('query');
268 5 50       165 if (scalar(@sparql) > 1) {
269 0         0 $content = "More than one query string submitted";
270 0         0 $self->log_error( $req, $content );
271 0         0 $self->_set_response_error($req, $response, 400, {
272             title => 'Multiple query strings not allowed',
273             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/multiple_queries',
274             });
275 0         0 goto CLEANUP;
276             } else {
277 5         15 $sparql = $sparql[0];
278             }
279             } elsif ($req->param('update')) {
280 2         96 my @sparql = $req->param('update');
281 2 50       71 if (scalar(@sparql) > 1) {
282 0         0 $content = "More than one update string submitted";
283 0         0 $self->log_error( $req, $content );
284 0         0 $self->_set_response_error($req, $response, 400, {
285             title => 'Multiple update strings not allowed',
286             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/multiple_updates',
287             });
288 0         0 goto CLEANUP;
289             }
290            
291 2 50 33     14 if ($config->{endpoint}{update} and $req->method eq 'POST') {
    0          
292 2         27 $sparql = $sparql[0];
293             } elsif ($req->method ne 'POST') {
294 0         0 my $method = $req->method;
295 0         0 $content = "Update operations must use POST";
296 0         0 $self->log_error( $req, $content );
297 0         0 $self->_set_response_error($req, $response, 405, {
298             title => "$method Not Allowed for Update Operation",
299             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/bad_http_method_update',
300             });
301 0         0 $response->header('Allow' => 'POST');
302 0         0 goto CLEANUP;
303             }
304             }
305            
306 10         222 my $ns = merge $config->{namespaces}, $NAMESPACES;
307              
308 10 100       119 if ($sparql) {
    50          
309 7         16 my %args;
310 7 100 66     61 $args{ update } = 1 if ($config->{endpoint}{update} and $req->method eq 'POST');
311 7 50       116 $args{ load_data } = 1 if ($config->{endpoint}{load_data});
312            
313             {
314 7         17 my @default = $req->param('default-graph-uri');
  7         25  
315 7         229 my @named = $req->param('named-graph-uri');
316 7 50 50     202 if (scalar(@default) or scalar(@named)) {
317 0         0 delete $args{ load_data };
318 0         0 $model = RDF::Trine::Model->new( RDF::Trine::Store::Memory->new() );
319 0         0 foreach my $url (@named) {
320 0         0 RDF::Trine::Parser->parse_url_into_model( $url, $model, context => iri($url) );
321             }
322 0         0 foreach my $url (@default) {
323 0         0 RDF::Trine::Parser->parse_url_into_model( $url, $model );
324             }
325             }
326             }
327            
328 7         24 my $protocol_specifies_update_dataset = 0;
329             {
330 7         18 my @default = $req->param('using-graph-uri');
  7         22  
331 7         167 my @named = $req->param('using-named-graph-uri');
332 7 50 50     203 if (scalar(@named) or scalar(@default)) {
333 0         0 $protocol_specifies_update_dataset = 1;
334 0         0 $model = RDF::Trine::Model::Dataset->new( $model );
335 0         0 $model->push_dataset( default => \@default, named => \@named );
336             }
337             }
338            
339 7   50     31 my $match = $headers->header('if-none-match') || '';
340 7         250 my $etag = md5_base64( join('#', $self->run_tag, $model->etag, $type, $ae, $sparql) );
341 7 50       276 if (length($match)) {
342 0 0 0     0 if (defined($etag) and ($etag eq $match)) {
343 0         0 $response->status(304);
344 0         0 return $response;
345             }
346             }
347            
348 7         34 my $base = $req->base;
349 7         1469 my $query = RDF::Query->new( $sparql, { lang => 'sparql11', base => $base, %args } );
350 7         65857 $self->log_query( $req, $sparql );
351 7 50       29 if ($query) {
352 7 50 33     29 if ($protocol_specifies_update_dataset and $query->specifies_update_dataset) {
353 0         0 my $method = $req->method;
354 0         0 $content = "Update operations cannot specify a dataset in both the query and with protocol parameters";
355 0         0 $self->log_error( $req, $content );
356 0         0 $self->_set_response_error($req, $response, 400, {
357             title => "Multiple datasets specified for update",
358             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/update_specifies_multiple_datasets',
359             detail => $content,
360             });
361 0         0 goto CLEANUP;
362             }
363 7         43 my ($plan, $ctx) = $query->prepare( $model );
364             # warn $plan->sse;
365 7         17808 my $iter = $query->execute_plan( $plan, $ctx );
366 7 50       30448 if ($iter) {
367 7         76 $response->status(200);
368 7 50       65 if (defined($etag)) {
369 7 50       35 if ($etag !~ /"/) {
370 7         28 $etag = qq["$etag"];
371             }
372 7 50       83 if ($etag =~ qr[^(W/)?"[\x{21}\x{23}-\x{7e}\x{80}-\x{FF}]*"$]) {
373 7         35 $response->headers->header( ETag => $etag );
374             } else {
375 0         0 warn "ETag value is not syntactically valid: " . Dumper($etag);
376             }
377             }
378 7 50       384 if ($iter->isa('RDF::Trine::Iterator::Graph')) {
379 0         0 my @variants = (['text/html', 0.99, 'text/html']);
380 0         0 my %media_types = %RDF::Trine::Serializer::media_types;
381 0         0 while (my($type, $sclass) = each(%media_types)) {
382 0 0       0 next if ($type =~ /html/);
383 0 0       0 my $value = ($type =~ m#application/rdf[+]xml#) ? 1.00 : 0.98;
384 0         0 push(@variants, [$type, $value, $type]);
385             }
386 0         0 my $stype = choose( \@variants, $headers );
387 0 0 0     0 if ($stype !~ /html/ and my $sclass = $RDF::Trine::Serializer::media_types{ $stype }) {
388 0         0 my $s = $sclass->new( namespaces => $ns );
389 0         0 $response->status(200);
390 0         0 $response->headers->content_type($stype);
391 0         0 $content = encode_utf8($s->serialize_iterator_to_string($iter));
392             } else {
393 0         0 $response->headers->content_type( 'text/html' );
394 0         0 my $html = $self->iter_as_html($iter, $model);
395 0         0 $content = encode_utf8($html);
396             }
397             } else {
398 7         84 my @variants = (
399             ['text/html', 0.99, 'text/html'],
400             ['application/sparql-results+xml', 1.0, 'application/sparql-results+xml'],
401             ['application/json', 0.95, 'application/json'],
402             ['application/rdf+xml', 0.95, 'application/rdf+xml'],
403             ['text/turtle', 0.95, 'text/turtle'],
404             ['text/xml', 0.8, 'text/xml'],
405             ['application/xml', 0.4, 'application/xml'],
406             ['text/plain', 0.2, 'text/plain'],
407             );
408 7   50     58 my $stype = choose( \@variants, $headers ) || 'application/sparql-results+xml';
409 7 100       4237 if ($stype =~ /html/) {
    50          
    0          
410 1         6 $response->headers->content_type( 'text/html' );
411 1         26 my $html = $self->iter_as_html($iter, $model, $sparql);
412 1         16 $content = encode_utf8($html);
413             } elsif ($stype =~ /xml/) {
414 6         27 $response->headers->content_type( $stype );
415 6         129 my $xml = $self->iter_as_xml($iter, $model);
416 6         4541 $content = encode_utf8($xml);
417             } elsif ($stype =~ /json/) {
418 0         0 $response->headers->content_type( $stype );
419 0         0 my $json = $self->iter_as_json($iter, $model);
420 0         0 $content = encode_utf8($json);
421             } else {
422 0         0 $response->headers->content_type( 'text/plain' );
423 0         0 my $text = $self->iter_as_text($iter, $model);
424 0         0 $content = encode_utf8($text);
425             }
426             }
427             } else {
428 0         0 my $error = $query->error;
429 0         0 $self->_set_response_error($req, $response, 500, {
430             title => "SPARQL query/update execution error",
431             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/execution_error',
432             detail => "$error; $sparql",
433             });
434 0         0 $content = RDF::Query->error;
435             }
436             } else {
437 0         0 $content = RDF::Query->error;
438 0         0 $self->log_error( $req, $content );
439 0 0       0 my $code = ($content =~ /Syntax/) ? 400 : 500;
440 0 0 0     0 if ($req->method ne 'POST' and $content =~ /read-only queries/sm) {
441 0         0 $content = 'Updates must use a HTTP POST request.';
442 0         0 $self->_set_response_error($req, $response, $code, {
443             title => $content,
444             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/bad_http_method_update',
445             });
446             } else {
447 0         0 $self->_set_response_error($req, $response, $code, {
448             title => "SPARQL query/update parse error",
449             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/parse_error',
450             detail => $content,
451             });
452             }
453             }
454             } elsif ($req->method eq 'POST') {
455 0         0 $content = "POST without recognized query or update";
456 0         0 $self->log_error( $req, $content );
457 0         0 $self->_set_response_error($req, $response, 400, {
458             title => "Missing SPARQL Query/Update String",
459             describedby => 'http://id.kasei.us/perl/rdf-endpoint/error/missing_sparql_string',
460             });
461             } else {
462 3         27 my @variants;
463 3         50 my %media_types = %RDF::Trine::Serializer::media_types;
464 3         22 while (my($type, $sclass) = each(%media_types)) {
465 33 50       71 next if ($type =~ /html/);
466 33         166 push(@variants, [$type, 0.99, $type]);
467             }
468 3         17 push(@variants, ['text/html', 1.0, 'text/html']);
469 3         20 my $stype = choose( \@variants, $headers );
470 3         1969 my $sdmodel = $self->service_description();
471 3 100 66     29 if ($stype !~ /html/ and my $sclass = $RDF::Trine::Serializer::media_types{ $stype }) {
472 1         15 my $s = $sclass->new( namespaces => $ns );
473 1         187 $response->status(200);
474 1         12 $response->headers->content_type($stype);
475 1         32 $content = encode_utf8($s->serialize_model_to_string($sdmodel));
476             } else {
477 2   50     15 my $dir = $ENV{RDF_ENDPOINT_SHAREDIR} || eval { dist_dir('RDF-Endpoint') } || 'share';
478 2         382 my $template = File::Spec->catfile($dir, 'index.html');
479 2         25 my $parser = XML::LibXML->new(validation => 0, suppress_errors => 1, no_network => 1, recover => 2) ;
480 2         261 my $doc = $parser->parse_file( $template );
481 2         1957 my $gen = RDF::RDFa::Generator->new( style => 'HTML::Head', namespaces => { %$ns } );
482 2         76884 $gen->inject_document($doc, $sdmodel);
483            
484 2         444922 my $writer = HTML::HTML5::Writer->new( markup => 'xhtml', doctype => DOCTYPE_XHTML_RDFA );
485 2         103 $content = encode_utf8( $writer->document($doc) );
486 2         191200 $response->status(200);
487 2         36 $response->headers->content_type('text/html');
488             }
489             }
490            
491 10   33     77071 CLEANUP:
492             # warn Dumper($model);
493             # warn $model->as_string;
494             $content = $response->body || $content;
495 10         1099 my $length = 0;
496 10         52 my %ae = map { $_ => 1 } split(/\s*,\s*/, $ae);
  10         67  
497 10 50       44 if ($ae{'gzip'}) {
498 10         29 my ($rh, $wh);
499 10         517 pipe($rh, $wh);
500 10 50       60 if (ref($content)) {
501 0         0 gzip $content => $wh;
502             } else {
503 10         99 gzip \$content => $wh;
504             }
505 10         25701 close($wh);
506 10         84 local($/) = undef;
507 10         251 my $body = <$rh>;
508 10         108 $length = bytes::length($body);
509 10         2022 $response->headers->header('Content-Encoding' => 'gzip');
510 10         550 $response->headers->header('Content-Length' => $length);
511 10 50       420 $response->body( $body ) unless ($req->method eq 'HEAD');
512             } else {
513 0         0 local($/) = undef;
514 0 0       0 my $body = ref($content) ? <$content> : $content;
515 0         0 $length = bytes::length($body);
516 0         0 $response->headers->header('Content-Length' => $length);
517 0 0       0 $response->body( $body ) unless ($req->method eq 'HEAD');
518             }
519 10         489 return $response;
520             }
521              
522             =item C<< run_tag >>
523              
524             Returns a unique key for each instantiation of this service.
525              
526             =cut
527              
528             sub run_tag {
529 7     7 1 13 my $self = shift;
530 7         101 return md5_base64(refaddr($self) . $self->{start_time});
531             }
532              
533             =item C<< service_description ( $request, $model ) >>
534              
535             Returns a new RDF::Trine::Model object containing a service description of this
536             endpoint, generating dataset statistics from C<< $model >>.
537              
538             =cut
539              
540             sub service_description {
541 5     5 1 14 my $self = shift;
542 5         21 my $model = $self->{model};
543 5   50     30 my $etag = $model->etag || '';
544            
545 5 100       200 if (exists $self->{ sd_cache }) {
546 3         31 my ($cached_etag, $model) = @{ $self->{ sd_cache } };
  3         15  
547 3 50 33     21 if (defined($cached_etag) and $etag eq $cached_etag) {
548 3         10 return $model;
549             }
550             }
551            
552 2         6 my $config = $self->{conf};
553 2         8 my $doap = RDF::Trine::Namespace->new('http://usefulinc.com/ns/doap#');
554 2         17 my $sd = RDF::Trine::Namespace->new('http://www.w3.org/ns/sparql-service-description#');
555 2         16 my $sde = RDF::Trine::Namespace->new('http://kasei.us/ns/service-description-extension#');
556 2         14 my $vann = RDF::Trine::Namespace->new('http://purl.org/vocab/vann/');
557 2         15 my $void = RDF::Trine::Namespace->new('http://rdfs.org/ns/void#');
558 2         16 my $scovo = RDF::Trine::Namespace->new('http://purl.org/NET/scovo#');
559 2         32 my $count = $model->count_statements( undef, undef, undef, RDF::Trine::Node::Nil->new );
560            
561 2         342 my @extensions = grep { !/kasei[.]us/ } RDF::Query->supported_extensions;
  20         65  
562 2         28 my @functions = grep { !/kasei[.]us/ } RDF::Query->supported_functions;
  86         573  
563 2         18 my @formats = keys %RDF::Trine::Serializer::format_uris;
564            
565 2         10 my $sdmodel = RDF::Trine::Model->temporary_model;
566 2         104 my $s = blank('service');
567 2         559 $sdmodel->add_statement( statement( $s, $rdf->type, $sd->Service ) );
568            
569 2         10906 $sdmodel->add_statement( statement( $s, $sd->supportedLanguage, $sd->SPARQL11Query ) );
570 2 50       2125 if ($config->{endpoint}{update}) {
571 2         17 $sdmodel->add_statement( statement( $s, $sd->supportedLanguage, $sd->SPARQL11Update ) );
572             }
573 2 50       1986 if ($config->{endpoint}{load_data}) {
574 0         0 $sdmodel->add_statement( statement( $s, $sd->feature, $sd->DereferencesURIs ) );
575             }
576            
577 2         8 foreach my $ext (@extensions) {
578 0         0 $sdmodel->add_statement( statement( $s, $sd->languageExtension, iri($ext) ) );
579             }
580 2         5 foreach my $func (@functions) {
581 84         82232 $sdmodel->add_statement( statement( $s, $sd->extensionFunction, iri($func) ) );
582             }
583            
584 2         1930 $sdmodel->add_statement( statement( $s, $sd->resultFormat, iri('http://www.w3.org/ns/formats/SPARQL_Results_XML') ) );
585 2         2005 $sdmodel->add_statement( statement( $s, $sd->resultFormat, iri('http://www.w3.org/ns/formats/SPARQL_Results_JSON') ) );
586 2         1915 foreach my $format (@formats) {
587 10         7546 $sdmodel->add_statement( statement( $s, $sd->resultFormat, iri($format) ) );
588             }
589            
590 2         1897 my $dataset = blank('dataset');
591 2         129 $sdmodel->add_statement( statement( $s, $sd->endpoint, iri('') ) );
592 2         1955 $sdmodel->add_statement( statement( $s, $sd->defaultDataset, $dataset ) );
593 2         1689 $sdmodel->add_statement( statement( $dataset, $rdf->type, $sd->Dataset ) );
594 2 50       2102 if (my $d = $config->{endpoint}{service_description}{default}) {
595 2 50       20 my $def_graph = ($d =~ /^\w+:/) ? iri($d) : blank('defaultGraph');
596 2         118 $sdmodel->add_statement( statement( $dataset, $sd->defaultGraph, $def_graph ) );
597 2         1666 $sdmodel->add_statement( statement( $def_graph, $rdf->type, $sd->Graph ) );
598 2         2026 $sdmodel->add_statement( statement( $def_graph, $rdf->type, $void->Dataset ) );
599 2         1953 $sdmodel->add_statement( statement( $def_graph, $void->triples, literal( $count, undef, $xsd->integer ) ) );
600             }
601 2 50       2170 if ($config->{endpoint}{service_description}{named_graphs}) {
602 2         42 my $iter = $model->get_contexts;
603 2         223 while (my $g = $iter->next) {
604 0         0 my $ng = blank();
605 0         0 my $graph = blank();
606 0         0 my $count = $model->count_statements( undef, undef, undef, $g );
607 0         0 $sdmodel->add_statement( statement( $dataset, $sd->namedGraph, $ng ) );
608 0         0 $sdmodel->add_statement( statement( $ng, $sd->name, $g ) );
609 0         0 $sdmodel->add_statement( statement( $ng, $sd->graph, $graph ) );
610 0         0 $sdmodel->add_statement( statement( $graph, $rdf->type, $sd->Graph ) );
611 0         0 $sdmodel->add_statement( statement( $graph, $rdf->type, $void->Dataset ) );
612 0         0 $sdmodel->add_statement( statement( $graph, $void->triples, literal( $count, undef, $xsd->integer ) ) );
613             }
614             }
615            
616 2 50       171 if (my $software = $config->{endpoint}{service_description}{software}) {
617 0         0 $sdmodel->add_statement( statement( $s, $sde->software, iri($software) ) );
618             }
619            
620 2 50       11 if (my $related = $config->{endpoint}{service_description}{related}) {
621 0         0 foreach my $r (@$related) {
622 0         0 $sdmodel->add_statement( statement( $s, $sde->relatedEndpoint, iri($r) ) );
623             }
624             }
625            
626 2 50       10 if (my $namespaces = $config->{endpoint}{service_description}{namespaces}) {
627 0         0 while (my($ns,$uri) = each(%$namespaces)) {
628 0         0 my $b = RDF::Trine::Node::Blank->new();
629 0         0 $sdmodel->add_statement( statement( $s, $sde->namespace, $b ) );
630 0         0 $sdmodel->add_statement( statement( $b, $vann->preferredNamespacePrefix, literal($ns) ) );
631 0         0 $sdmodel->add_statement( statement( $b, $vann->preferredNamespaceUri, literal($uri) ) );
632             }
633             }
634            
635 2         10 $self->{ sd_cache } = [ $etag, $sdmodel ];
636 2         32 return $sdmodel;
637             }
638              
639             =begin private
640              
641             =item C<< iter_as_html ( $iter, $model ) >>
642              
643             =cut
644              
645             sub iter_as_html {
646 1     1 1 2 my $self = shift;
647 1         2 my $stream = shift;
648 1         3 my $model = shift;
649 1         2 my $query = shift;
650              
651 1   50     10 my $dir = $ENV{RDF_ENDPOINT_SHAREDIR} || eval { dist_dir('RDF-Endpoint') } || 'share';
652 1         205 my $file = File::Spec->catfile($dir, 'results.html');
653 1         5 my $html;
654              
655 1 50       25 if (-r $file) {
656 1 50       83 open( my $fh, '<', $file ) or croak $!;
657 1         3 $html = do { local $/; <$fh>; };
  1         8  
  1         44  
658 1         19 close $fh;
659             } else {
660 0         0 $html = <<HTML
661             <html><head><title>SPARQL Results</title></head><body>
662             <div id="result" />
663             <h2>Query</h2>
664             <form id="queryform" action="" method="get">
665             <p><textarea id="query" name="query" rows="10" cols="60"></textarea>
666             <br/>
667             <select id="media-type" name="media-type">
668             <option value="">Result Format...</option>
669             <option label="HTML" value="text/html">HTML</option>
670             <option label="Turtle" value="text/turtle">Turtle</option>
671             <option label="XML" value="text/xml">XML</option>
672             <option label="JSON" value="application/json">JSON</option>
673             </select>
674             <input name="submit" id="submit" type="submit" value="Submit" />
675             </p>
676             </form>
677             </body></html>
678             HTML
679             }
680              
681 1         4 my $result = "<h2>Result</h2>\n";
682              
683 1 50       18 if ($stream->isa('RDF::Trine::Iterator::Boolean')) {
    50          
684 0 0       0 $result = (($stream->get_boolean) ? "True" : "False");
685             } elsif ($stream->isa('RDF::Trine::Iterator::Bindings')) {
686 1         3 $result = "<table class='tablesorter'>\n<thead><tr>\n";
687            
688 1         6 my @names = $stream->binding_names;
689 1         12 my $columns = scalar(@names);
690 1         4 foreach my $name (@names) {
691 3         8 $result .= "\t<th>" . $name . "</th>\n";
692             }
693 1         6 $result .= "</tr></thead>\n";
694            
695 1         4 my $count = 0;
696 1         16 while (my $row = $stream->next) {
697 3         1881 $count++;
698 3         6 $result .= "<tr>\n";
699 3         8 foreach my $k (@names) {
700 9         15 my $node = $row->{ $k };
701 9         33 my $value = $self->node_as_html($node, $model);
702 9         30 $result .= "\t<td>" . $value . "</td>\n";
703             }
704 3         13 $result .= "</tr>\n";
705             }
706 1         163 $result .= "<tfoot><tr><th colspan=\"$columns\">Total: $count</th></tr></tfoot>\n</table>\n";
707             }
708              
709 1         20 $html =~ s/<div\s+id\s*=\s*["']result["']\s*\/>/<div id="result">$result<\/div>/;
710 1         54 $html =~ s/(<textarea[^>]*>)(.|\n)*(<\/textarea>)/$1$query$3/sm;
711              
712 1         6 return $html;
713             }
714              
715             =item C<< iter_as_text ( $iter ) >>
716              
717             =cut
718              
719             sub iter_as_text {
720 0     0 1 0 my $self = shift;
721 0         0 my $iter = shift;
722 0 0       0 if ($iter->isa('RDF::Trine::Iterator::Graph')) {
723 0         0 my $serializer = RDF::Trine::Serializer->new('ntriples');
724 0         0 return $serializer->serialize_iterator_to_string( $iter );
725             } else {
726 0         0 return $iter->as_string;
727             }
728             }
729              
730             =item C<< iter_as_xml ( $iter ) >>
731              
732             =cut
733              
734             sub iter_as_xml {
735 6     6 1 13 my $self = shift;
736 6         12 my $iter = shift;
737 6         32 return $iter->as_xml;
738             }
739              
740             =item C<< iter_as_json ( $iter ) >>
741              
742             =cut
743              
744             sub iter_as_json {
745 0     0 1 0 my $self = shift;
746 0         0 my $iter = shift;
747 0         0 return $iter->as_json;
748             }
749              
750             =item C<< node_as_html ( $node, $model ) >>
751              
752             =cut
753              
754             sub node_as_html {
755 9     9 1 14 my $self = shift;
756 9         15 my $node = shift;
757 9         13 my $model = shift;
758 9         14 my $config = $self->{conf};
759 9 50       38 return '' unless (blessed($node));
760 9 100       50 if ($node->isa('RDF::Trine::Node::Resource')) {
    100          
761 6         16 my $uri = $node->uri_value;
762 6         29 for ($uri) {
763 6         26 s/&/&amp;/g;
764 6         14 s/</&lt;/g;
765             }
766 6         13 my $link = $config->{endpoint}{html}{resource_links};
767 6         9 my $html;
768 6 50       15 if ($config->{endpoint}{html}{embed_images}) {
769 0 0       0 if ($model->count_statements( $node, iri('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), iri('http://xmlns.com/foaf/0.1/Image') )) {
770 0   0     0 my $width = $config->{endpoint}{html}{image_width} || 200;
771 0         0 $html = qq[<img src="${uri}" width="${width}" />];
772             } else {
773 0         0 $html = $uri;
774             }
775             } else {
776 6         9 $html = $uri;
777             }
778 6 50       12 if ($link) {
779 6         20 $html = qq[<a href="${uri}">$html</a>];
780             }
781 6         15 return $html;
782             } elsif ($node->isa('RDF::Trine::Node::Literal')) {
783 2         6 my $html = $node->literal_value;
784 2         20 for ($html) {
785 2         4 s/&/&amp;/g;
786 2         5 s/</&lt;/g;
787             }
788 2         5 return $html;
789             } else {
790 1         6 my $html = $node->as_string;
791 1         14 for ($html) {
792 1         4 s/&/&amp;/g;
793 1         3 s/</&lt;/g;
794             }
795 1         4 return $html;
796             }
797             }
798              
799             =item C<< log_query ( $message ) >>
800              
801             =cut
802              
803             sub log_query {
804 7     7 1 18 my $self = shift;
805 7         14 my $req = shift;
806 7         18 my $message = shift;
807 7         51 $self->_log( $req, { level => 'info', message => $message } );
808             }
809              
810             =item C<< log_error ( $message ) >>
811              
812             =cut
813              
814             sub log_error {
815 0     0 1 0 my $self = shift;
816 0         0 my $req = shift;
817 0         0 my $message = shift;
818 0         0 $self->_log( $req, { level => 'error', message => $message } );
819             }
820              
821             sub _log {
822 7     7   19 my $self = shift;
823 7         13 my $req = shift;
824 7         14 my $data = shift;
825 7   50 7   35 my $logger = $req->logger || sub {};
826            
827 7         120 $logger->($data);
828             }
829              
830             sub _set_response_error {
831 0     0     my $self = shift;
832 0           my $req = shift;
833 0           my $resp = shift;
834 0           my $code = shift;
835 0           my $error = shift;
836 0           my @variants = (
837             ['text/plain', 1.0, 'text/plain'],
838             ['application/json-problem', 0.99, 'application/json-problem'],
839             );
840 0           my $headers = $req->headers;
841 0   0       my $stype = choose( \@variants, $headers ) || 'text/plain';
842 0 0         if ($stype eq 'application/json-problem') {
843 0           $resp->headers->content_type( 'application/json-problem' );
844 0           $resp->status($code);
845 0           my $content = encode_json($error);
846 0           $resp->body($content);
847             } else {
848 0           $resp->headers->content_type( 'text/plain' );
849 0           $resp->status($code);
850 0           my @messages = grep { defined($_) } @{ $error }{ qw(title detail) };
  0            
  0            
851 0           my $content = join("\n\n", @messages);
852 0           $resp->body($content);
853             }
854 0           return;
855             }
856              
857             =end private
858              
859             =cut
860              
861             1;
862              
863             __END__
864              
865             =back
866              
867             =head1 SEE ALSO
868              
869             =over 4
870              
871             =item * L<http://www.w3.org/TR/sparql11-protocol/>
872              
873             =item * L<http://www.perlrdf.org/>
874              
875             =item * L<irc://irc.perl.org/#perlrdf>
876              
877             =item * L<http://codemirror.net/>
878              
879             =back
880              
881             =head1 AUTHOR
882              
883             Gregory Todd Williams <gwilliams@cpan.org>
884              
885             =head1 LICENSE AND COPYRIGHT
886              
887             Copyright (c) 2010-2014 Gregory Todd Williams.
888              
889             This software is provided 'as-is', without any express or implied
890             warranty. In no event will the authors be held liable for any
891             damages arising from the use of this software.
892              
893             Permission is granted to anyone to use this software for any
894             purpose, including commercial applications, and to alter it and
895             redistribute it freely, subject to the following restrictions:
896              
897             1. The origin of this software must not be misrepresented; you must
898             not claim that you wrote the original software. If you use this
899             software in a product, an acknowledgment in the product
900             documentation would be appreciated but is not required.
901              
902             2. Altered source versions must be plainly marked as such, and must
903             not be misrepresented as being the original software.
904              
905             3. This notice may not be removed or altered from any source
906             distribution.
907              
908             With the exception of the CodeMirror files, the files in this package may also
909             be redistributed and/or modified under the same terms as Perl itself.
910              
911             The CodeMirror (Javascript and CSS) files contained in this package are
912             copyright (c) 2007-2010 Marijn Haverbeke, and licensed under the terms of the
913             same zlib license as this code.
914              
915             =cut