From: tla Date: Mon, 28 Oct 2013 15:32:49 +0000 (+0100) Subject: make Stemweb listener for result notification (#27) Relies on tla/stemmatology@79c81b... X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=scpubgit%2Fstemmaweb.git;a=commitdiff_plain;h=532cc23bba52829b6205df42f39294949bc5a3db make Stemweb listener for result notification (#27) Relies on tla/stemmatology@79c81b71c5c64463217c0120d0061a25d67b2453 --- diff --git a/lib/stemmaweb.pm b/lib/stemmaweb.pm index 7b52ebd..4a8f77a 100644 --- a/lib/stemmaweb.pm +++ b/lib/stemmaweb.pm @@ -23,7 +23,6 @@ use Search::GIN::Extract::Multiplex; use Catalyst qw/ ConfigLoader Static::Simple - Unicode::Encoding Authentication Session Session::Store::File @@ -49,6 +48,8 @@ __PACKAGE__->config( name => 'stemmaweb', # Disable deprecated behavior needed by old applications disable_component_resolution_regex_fallback => 1, + # Set Unicode as the default + encoding => 'UTF-8', default_view => 'TT', 'View::JSON' => { expose_stash => 'result', diff --git a/lib/stemmaweb/Controller/Stemweb.pm b/lib/stemmaweb/Controller/Stemweb.pm new file mode 100644 index 0000000..8aa8786 --- /dev/null +++ b/lib/stemmaweb/Controller/Stemweb.pm @@ -0,0 +1,105 @@ +package stemmaweb::Controller::Stemweb; +use Moose; +use namespace::autoclean; +use JSON qw/ from_json /; +use Safe::Isa; +use TryCatch; + +BEGIN { extends 'Catalyst::Controller' } + +=head1 NAME + +stemmaweb::Controller::Stemweb - Client listener for Stemweb results + +=head1 DESCRIPTION + +This is a client listener for the Stemweb API as implemented by the protocol defined at +L. + +=head1 METHODS + +=head2 result + + POST stemweb/result + Content-Type: application/json + (On success): + { job_id: + status: 0 + format: + result: } + (On failure): + { jobid: + status: >1 + result: } + +Used by the Stemweb server to notify us that one or more stemma graphs +has been calculated in response to an earlier request. + +=cut + +sub result :Local :Args(0) { + my( $self, $c ) = @_; + if( $c->request->method eq 'POST' ) { + # TODO: Verify the sender! + my $answer; + if( ref( $c->request->body ) eq 'File::Temp' ) { + # Read in the file and parse that. + open( POSTDATA, $c->request->body ) or die "Failed to open post data file"; + binmode( POSTDATA, ':utf8' ); + # JSON should be all one line + my $pdata = ; + chomp $pdata; + close POSTDATA; + $answer = from_json( $pdata ); + } else { + $answer = from_json( $c->request->body ); + } + # Find a tradition with the defined Stemweb job ID. + # TODO: Maybe get Stemweb to pass back the tradition ID... + my $m = $c->model('Directory'); + my @traditions; + $m->scan( sub{ push( @traditions, $_[0] ) + if $_[0]->$_isa('Text::Tradition') + && $_[0]->has_stemweb_jobid + && $_[0]->stemweb_jobid eq $answer->{job_id}; + } ); + if( @traditions == 1 ) { + my $tradition = shift @traditions; + if( $answer->{status} == 0 ) { + try { + $tradition->record_stemweb_result( $answer ); + $m->save( $tradition ); + } catch( Text::Tradition::Error $e ) { + return _json_error( $c, 500, $e->message ); + } catch { + return _json_error( $c, 500, $@ ); + } + # If we got here, success! + $c->stash->{'result'} = { 'status' => 'success' }; + $c->forward('View::JSON'); + } else { + return _json_error( $c, 500, + "Stemweb failure not handled: " . $answer->{result} ); + } + } elsif( @traditions ) { + return _json_error( $c, 500, + "Multiple traditions with Stemweb job ID " . $answer->{job_id} . "!" ); + } else { + return _json_error( $c, 400, + "No tradition found with Stemweb job ID " . $answer->{job_id} ); + } + } else { + return _json_error( $c, 403, 'Please use POST!' ); + } +} + +# Helper to throw a JSON exception +sub _json_error { + my( $c, $code, $errmsg ) = @_; + $c->response->status( $code ); + $c->stash->{'result'} = { 'error' => $errmsg }; + $c->forward('View::JSON'); + return 0; +} + +1; \ No newline at end of file diff --git a/script/maketestdb.pl b/script/maketestdb.pl index 387eeba..389b8ec 100755 --- a/script/maketestdb.pl +++ b/script/maketestdb.pl @@ -39,6 +39,7 @@ say "Created users"; my $t1 = Text::Tradition->new( input => 'Self', file => 't/data/besoin.xml' ); die "Failed to create test tradition #1" unless $t1; +$t1->add_stemma( dotfile => 't/data/besoin_stemweb.dot' ); $user->add_tradition( $t1 ); $dir->store( $user ); say "Created test user tradition"; diff --git a/t/controller_Stemweb.t b/t/controller_Stemweb.t new file mode 100644 index 0000000..49b1023 --- /dev/null +++ b/t/controller_Stemweb.t @@ -0,0 +1,148 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use feature 'say'; +use HTTP::Request::Common; +use JSON qw/ decode_json /; +use Test::More; +use Test::WWW::Mechanize::Catalyst; +use Text::Tradition::Directory; + +use Catalyst::Test 'stemmaweb'; +use LWP::UserAgent; + +eval { no warnings; $DB::deep = 1000; }; + +# Set up the test data +use vars qw( $orig_db $was_link ); +my $textids; +my $dbfile = 'db/traditions.db'; +( $orig_db, $was_link, $textids ) = _make_testing_database(); + +# Here is the test data we will use +my $answer = '{"status": 0, "job_id": "4", "algorithm": "RHM", "format": "newick", "start_time": "2013-10-26 10:44:14.050263", "result": "((((((((((((F,U),V),S),T1),T2),A),J),B),L),D),M),C);\n", "end_time": "2013-10-26 10:45:55.398944"}'; + +# First try without a job ID in the database anywhere +my $unclaimed_request = request POST '/stemweb/result', + 'Content-Type' => 'application/json', + 'Content' => $answer; +like( $unclaimed_request->header('Content-Type'), qr/application\/json/, + "Returned JSON answer for unclaimed request" ); +is( $unclaimed_request->code, 400, "No tradition found with given job ID" ); +like( $unclaimed_request->content, qr/No tradition found with Stemweb job ID/, + "Correct error message returned" ); + +# Now add the relevant job ID to two traditions and test for that error +{ + my $dsn = "dbi:SQLite:dbname=$dbfile"; + my $dir = Text::Tradition::Directory->new( 'dsn' => $dsn ); + my $scope = $dir->new_scope(); + my $t1 = $dir->lookup( $textids->{'public'} ); + $t1->set_stemweb_jobid( '4' ); + $dir->save( $t1 ); + my $t2 = $dir->lookup( $textids->{'private'} ); + $t2->set_stemweb_jobid( '4' ); + $dir->save( $t2 ); +} +# Now try with the job ID in more than one place in the database +my $oversubscribed_request = request POST '/stemweb/result', + 'Content-Type' => 'application/json', + 'Content' => $answer; +like( $oversubscribed_request->header('Content-Type'), qr/application\/json/, + "Returned JSON answer for oversubscribed request" ); +is( $oversubscribed_request->code, 500, "Multiple traditions found with given job ID" ); +like( $oversubscribed_request->content, qr/Multiple traditions with Stemweb job ID/, + "Correct error message returned" ); + +# Finally, try with the job ID on only one tradition. +{ + my $dsn = "dbi:SQLite:dbname=$dbfile"; + my $dir = Text::Tradition::Directory->new( 'dsn' => $dsn ); + my $scope = $dir->new_scope(); + my $t2 = $dir->lookup( $textids->{'private'} ); + $t2->_clear_stemweb_jobid; + $dir->save( $t2 ); +} +my $expected_request = request POST '/stemweb/result', + 'Content-Type' => 'application/json', + 'Content' => $answer; +like( $expected_request->header('Content-Type'), qr/application\/json/, + "Returned JSON answer for expected request" ); +is( $expected_request->code, 200, "Request processed successfully" ); +like( $expected_request->content, qr/success/, + "Correct success message returned" ); + +# Now check that the tradition in question actually has a stemma! +my $stemma_request = request('/stemmadot/' . $textids->{'public'} . '/0' ); +like( $stemma_request->header('Content-Type'), qr/application\/json/, + "Returned JSON answer for stemma request" ); +my $new_stemma = decode_json( $stemma_request->content ); +# It will be undirected. +like( $new_stemma->{dot}, qr/^graph .RHM 1382777054_0/, + "Found an undirected stemma DOT file where we expected" ); + +# And check that the job ID was in fact removed. +my $duplicate_request = request POST '/stemweb/result', + 'Content-Type' => 'application/json', + 'Content' => $answer; +like( $duplicate_request->header('Content-Type'), qr/application\/json/, + "Returned JSON answer for duplicate request" ); +like( $duplicate_request->content, qr/No tradition found with Stemweb job ID/, + "Job ID was removed from relevant tradition" ); + +done_testing(); + + +sub _make_testing_database { + my $fh = File::Temp->new(); + my $file = $fh->filename; + $fh->unlink_on_destroy( 0 ); + $fh->close; + my $dsn = 'dbi:SQLite:dbname=' . $file; + my $dir = Text::Tradition::Directory->new( 'dsn' => $dsn, + 'extra_args' => { 'create' => 1 } ); + my $scope = $dir->new_scope; + + my $textids = {}; + # Create a (public) tradition + my $pubtrad = Text::Tradition->new( input => 'Self', file => 't/data/besoin.xml' ); + $pubtrad->public( 1 ); + $textids->{'public'} = $dir->store( $pubtrad ); + + # Create a user + my $adminobj = $dir->add_user( { username => 'stadmin', password => 'stadminpass', role => 'admin' } ); + my $userobj = $dir->add_user( { username => 'swtest', password => 'swtestpass' } ); + # Create a tradition for the normal user + my $privtrad = Text::Tradition->new( input => 'Tabular', sep_char => ',', + file => 't/data/florilegium.csv' ); + $userobj->add_tradition( $privtrad ); + $privtrad->add_stemma( dotfile => 't/data/florilegium.dot' ); + $textids->{'private'} = $dir->store( $privtrad ); + $dir->store( $userobj ); + + ## Now replace the existing traditions database with the test one + my( $orig, $was_link ); + if( -l $dbfile ) { + $was_link = 1; + $orig = readlink( $dbfile ); + unlink( $dbfile ) or die "Could not replace database file $dbfile"; + } elsif( -e $dbfile ) { + my $suffix = '.backup.' . time(); + $orig = $dbfile.$suffix; + rename( $dbfile, $orig ) or die "Could not move database file $dbfile"; + } + symlink( $file, $dbfile ); + return( $orig, $was_link, $textids ); +} + +END { + # Restore the original database + unlink( readlink( $dbfile ) ); + unlink( $dbfile ); + if( $was_link ) { + symlink( $orig_db, $dbfile ); + } elsif( $orig_db ) { + rename( $orig_db, $dbfile ); + } +} diff --git a/t/data/besoin_stemweb.dot b/t/data/besoin_stemweb.dot new file mode 100644 index 0000000..75df083 --- /dev/null +++ b/t/data/besoin_stemweb.dot @@ -0,0 +1,52 @@ +graph stemma { + 0 [ class=hypothetical ]; + 1 [ class=hypothetical ]; + 10 [ class=hypothetical ]; + 11 [ class=hypothetical ]; + 2 [ class=hypothetical ]; + 3 [ class=hypothetical ]; + 4 [ class=hypothetical ]; + 5 [ class=hypothetical ]; + 6 [ class=hypothetical ]; + 7 [ class=hypothetical ]; + 8 [ class=hypothetical ]; + 9 [ class=hypothetical ]; + A [ class=extant ]; + B [ class=extant ]; + C [ class=extant ]; + D [ class=extant ]; + F [ class=extant ]; + J [ class=extant ]; + L [ class=extant ]; + M [ class=extant ]; + S [ class=extant ]; + T1 [ class=extant ]; + T2 [ class=extant ]; + U [ class=extant ]; + V [ class=extant ]; + 0 -- 1; + 0 -- C; + 10 -- 11; + 10 -- 9; + 10 -- V; + 11 -- F; + 11 -- U; + 1 -- 2; + 1 -- M; + 2 -- 3; + 2 -- D; + 3 -- 4; + 4 -- 5; + 5 -- 6; + 6 -- 7; + 7 -- 8; + 8 -- 9; + A -- 6; + B -- 4; + J -- 5; + L -- 3; + S -- 9; + T1 -- 8; + T2 -- 7; +} +