File Coverage

blib/lib/RDF/Endpoint.pm
Criterion Covered Total %
statement 30 32 93.7
branch n/a
condition n/a
subroutine 11 11 100.0
pod n/a
total 41 43 95.3


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