use strict; use warnings; package Plack::App::DAIA; { $Plack::App::DAIA::VERSION = '0.48'; } #ABSTRACT: DAIA Server as Plack application use v5.10.1; use parent 'Plack::Component'; use LWP::Simple qw(get); use Encode; use JSON; use DAIA; use Scalar::Util qw(blessed); use Plack::Util::Accessor qw(xslt warnings errors code idformat initialized html); use Plack::Middleware::Static; use File::ShareDir qw(dist_dir); use Plack::Request; our %FORMATS = DAIA->formats; sub prepare_app { my $self = shift; return if $self->initialized; $self->errors(0) unless defined $self->errors; $self->warnings(1) if $self->errors or not defined $self->warnings; $self->idformat( $self->IDFORMAT) unless defined $self->idformat; $self->init; if ($self->html) { $self->html( Plack::Middleware::Static->new( path => qr{daia\.(xsl|css|xsd)$|xmlverbatim\.xsl$|icons/[a-z0-9_-]+\.png$}, root => dist_dir('Plack-App-DAIA') )); $self->xslt( 'daia.xsl' ) unless $self->xslt; # TODO: fix base path } $self->initialized(1); } sub init { # initialization hook } sub IDFORMAT { qr{^.*$} }; sub call { my ($self, $env) = @_; my $req = Plack::Request->new($env); my $id = $req->param('id') // ''; my $format = lc($req->param('format') // ''); # serve HTML client if ( $self->html and $id eq '' ) { my $resp = $self->html->_handle_static( $env ); if ($resp and $resp->[0] eq 200) { return $resp; } } # validate identifier my ($invalid_id, $message, %parts) = ('',undef); if ( $id ne '' and ref $self->idformat ) { if ( ref $self->idformat eq 'Regexp' ) { if ( $id =~ $self->idformat ) { %parts = %+; # named capturing groups } else { $invalid_id = $id; $id = ""; } } } if ( $self->warnings ) { if ( $invalid_id ne '' ) { $message = 'unknown identifier format'; } elsif ( $id eq '' ) { $message = 'please provide a document identifier'; } } # retrieve and construct response my ($status, $daia) = (200, undef); if ( $message and $self->errors ) { $daia = DAIA::Response->new; } else { $daia = $self->retrieve( $id, %parts ); if (!$daia) { $daia = DAIA::Response->new; $status = 500; } } if ( $message and $self->warnings ) { $daia->addMessage( 'en' => $message, errno => 400 ); } $self->as_psgi( $status, $daia, $format, $req->param('callback') ); } sub retrieve { my $self = shift; return $self->code ? $self->code->(@_) : undef; } sub as_psgi { my ($self, $status, $daia, $format, $callback) = @_; my ($content, $type); $type = $FORMATS{$format} unless $format eq 'xml'; $content = $daia->serialize($format) if $type; if (!$content) { $type = "application/xml; charset=utf-8"; if ( $self->warnings ) { if ( not $format ) { $daia->addMessage( 'en' => 'please provide an explicit parameter format=xml', 300 ); } elsif ( $format ne 'xml' ) { $daia->addMessage( 'en' => 'unknown or unsupported format', 300 ); } } $content = $daia->xml( header => 1, xmlns => 1, ( $self->xslt ? (xslt => $self->xslt) : () ) ); } elsif ( $type =~ qr{^application/javascript} and ($callback || '') =~ /^[\w\.\[\]]+$/ ) { $content = "$callback($content)"; } return [ $status, [ "Content-Type" => $type ], [ encode('utf8',$content) ] ]; } 1; __END__ =pod =head1 NAME Plack::App::DAIA - DAIA Server as Plack application =head1 VERSION version 0.48 =head1 SYNOPSIS Either derive from Plack::App::DAIA package Your::App; use parent 'Plack::App::DAIA'; sub IDFORMAT { qr{^[a-z]+:.*$} } sub retrieve { my ($self, $id, %idparts) = @_; my $daia = DAIA::Response->new; # construct full response ... return $daia; }; 1; or pass a code reference as option C: use Plack::App::DAIA; Plack::App::DAIA->new( code => sub { my ($id, %idparts) = @_; my $daia = DAIA::Response->new; # construct full response ... return $daia; }, idformat => qr{^[a-z]+:.*$} ); =head1 DESCRIPTION This module implements a B (L) server as PSGI application. The application receives two URL parameters: =over 4 =item B refers to the document to retrieve availability information. The id is parsed based on the L option and passed to an internal L method, which must return a L object. =item B specifies a DAIA serialization format, that the resulting L is returned in. By default the formats C (DAIA/XML, the default), C (DAIA/JSON>, and C (DAIA/RDF in RDF/JSON) are supported. Additional RDF serializations (C, C, and C) are supported if L is installed. If L is installed, the RDF/Turtle output uses well-known namespace prefixes. Visual RDF graphs are supported with format C and C if L is installed and C is in C<$ENV{PATH}>. =back This module automatically adds some warnings and error messages and provides a simple HTML interface based on client side XSLT. =head1 METHODS =head2 new ( [%options] ) Creates a new DAIA server. Supported options are =over 4 =item xslt Path of a DAIA XSLT client to attach to DAIA/XML responses. Not set by default and set to C if option C is set. You still may need to adjust the path if your server rewrites the request path. =item html Enable a HTML client for DAIA/XML via XSLT. The client is returned in form of three files (C, C, C) and DAIA icons, all shipped together with this module. Enabling HTML client also enables serving the DAIA XML Schema as C. =item warnings Enable warnings in the DAIA response (enabled by default). =item errors Enable warnings and directly return a response without calling the retrieve method on error. =item code Code reference to the C method if you prefer not to create a module derived from this module. =item idformat Optional regular expression to validate identifiers. Invalid identifiers are set to the empty string before they are passed to the C method. In addition an error message "unknown identifier format" is added to the response, if the option C are enabled. If the option C is enabled, the C method is not called on error. It is recommended to use regular expressions with named capturing groups as introduced in Perl 5.10. The named parts are also passed to the C. For instance: idformat => qr{^ (?[a-z]+) : (?.+) $}x will give you C<$parts{prefix}> and C<$parts{local}> in the retrieve method. =item initialized Stores whether the application had been initialized. =back =head2 retrieve ( $id [, %parts ] ) Must return a status and a L object. Override this method if you derive an application from Plack::App::DAIA. By default it either calls the retrieve code, as passed to the constructor, or returns undef, so a HTTP 500 error is returned. This method is passed the original query identifier and a hash of named capturing groups from your identifier format. =head2 init This method is called by Plack::Component::prepare_app, once before the first request. You can define this method in you subclass as initialization hook, for instance to set default option values. Initialization during runtime can be triggered by setting C to false. =head2 as_psgi ( $status, $daia [, $format [, $callback ] ] ) Serializes a L in some DAIA serialization format (C by default) and returns a a PSGI response with given HTTP status code. =head2 IDFORMAT Returns the default idformat for this module. =head1 EXAMPLES You can also mix this application with L middleware. It is highly recommended to test your services! Testing is made as easy as possible with the L command line script. This module contains a dummy application C and a more detailed example C. =head1 SEE ALSO Plack::App::DAIA is derived from L. Use L and L (using L) for writing tests. See L for a DAIA validator and converter. =head1 AUTHOR Jakob Voss =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2012 by Jakob Voss. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut